Files
budget-app/scripts/setup-appwrite.cjs
Claude 37bc35f53f feat: initial BudgetWise budgeting app with Appwrite sync
Full-stack PWA budgeting app installable on Android via Chrome.

Features implemented:
- Monthly balance sheet: income sources (monthly/yearly), buffer (fixed $ or % of income)
- Buckets: regular, savings-goal, and investment types with custom goals
  (amount or % of income, monthly or yearly schedule)
- Debt tracker: manual or auto-EMI (amortization formula), interest rate,
  remaining balance tracking, avalanche-method repayment suggestions
- Loan calculator: max affordable principal and EMI checker based on
  remaining income after all expenses
- Investment projections: compound interest at monthly/yearly rate,
  1/5/10-year projections per bucket
- Deposit / withdraw transactions per bucket with balance history
- Appwrite self-hosted backend: auth, 5 collections (balance_sheets,
  incomes, buckets, debts, transactions)
- scripts/setup-appwrite.cjs: one-command DB setup via node-appwrite
- PWA manifest + service worker (vite-plugin-pwa) for Android install
- Dark mobile-first UI with TailwindCSS, Zustand state, React Router v6

https://claude.ai/code/session_01Ny2EMaZYvzk5SSVDAPgxpP
2026-02-22 14:10:44 +00:00

246 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* BudgetWise Appwrite database setup script
*
* Run once to create the database, collections, and attributes
* in your self-hosted Appwrite instance.
*
* Usage:
* cp .env.example .env
* # Fill in VITE_APPWRITE_ENDPOINT, VITE_APPWRITE_PROJECT_ID, APPWRITE_API_KEY
* npm run setup:appwrite
*/
const { Client, Databases, Permission, Role, IndexType } = require('node-appwrite');
const fs = require('fs');
const path = require('path');
// ── Load .env manually (no dotenv dependency needed) ─────────────────────────
function loadEnv() {
const envPath = path.join(__dirname, '..', '.env');
if (!fs.existsSync(envPath)) {
console.error('❌ .env file not found. Copy .env.example to .env and fill in the values.');
process.exit(1);
}
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
const env = {};
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const [key, ...rest] = trimmed.split('=');
env[key.trim()] = rest.join('=').trim();
}
return env;
}
const env = loadEnv();
const ENDPOINT = env.VITE_APPWRITE_ENDPOINT;
const PROJECT_ID = env.VITE_APPWRITE_PROJECT_ID;
const API_KEY = env.APPWRITE_API_KEY;
if (!ENDPOINT || !PROJECT_ID || !API_KEY) {
console.error('❌ Missing VITE_APPWRITE_ENDPOINT, VITE_APPWRITE_PROJECT_ID, or APPWRITE_API_KEY in .env');
process.exit(1);
}
const client = new Client()
.setEndpoint(ENDPOINT)
.setProject(PROJECT_ID)
.setKey(API_KEY);
const databases = new Databases(client);
const DB_ID = 'budget_db';
const DB_NAME = 'BudgetWise';
// ── Permissions: only authenticated users can access their own docs ──────────
// We rely on document-level security with user_id filtering.
const PERMISSIONS = [
Permission.read(Role.users()),
Permission.create(Role.users()),
Permission.update(Role.users()),
Permission.delete(Role.users()),
];
async function createCollection(id, name, attrs, indexes = []) {
console.log(`\n📁 Creating collection: ${name}`);
try {
await databases.createCollection(DB_ID, id, name, PERMISSIONS);
console.log(` ✓ Collection created`);
} catch (err) {
if (err.code === 409) {
console.log(` ⚠ Collection already exists, skipping...`);
} else {
throw err;
}
}
// Add attributes
for (const attr of attrs) {
try {
if (attr.type === 'string') {
await databases.createStringAttribute(DB_ID, id, attr.key, attr.size ?? 255, attr.required ?? false, attr.default ?? null);
} else if (attr.type === 'float') {
await databases.createFloatAttribute(DB_ID, id, attr.key, attr.required ?? false, undefined, undefined, attr.default ?? null);
} else if (attr.type === 'integer') {
await databases.createIntegerAttribute(DB_ID, id, attr.key, attr.required ?? false, undefined, undefined, attr.default ?? null);
} else if (attr.type === 'boolean') {
await databases.createBooleanAttribute(DB_ID, id, attr.key, attr.required ?? false, attr.default ?? null);
}
process.stdout.write(`${attr.key}\n`);
// Wait a bit between attributes (Appwrite rate limits)
await sleep(300);
} catch (err) {
if (err.code === 409) {
process.stdout.write(`${attr.key} (already exists)\n`);
} else {
console.error(`${attr.key}: ${err.message}`);
}
}
}
// Add indexes
for (const idx of indexes) {
try {
await databases.createIndex(DB_ID, id, idx.key, IndexType.Key, idx.attributes, idx.orders ?? []);
process.stdout.write(` ✓ index: ${idx.key}\n`);
await sleep(300);
} catch (err) {
if (err.code === 409) {
process.stdout.write(` ⚠ index ${idx.key} (already exists)\n`);
} else {
console.error(` ❌ index ${idx.key}: ${err.message}`);
}
}
}
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function main() {
console.log('🚀 BudgetWise Appwrite Setup');
console.log(` Endpoint : ${ENDPOINT}`);
console.log(` Project : ${PROJECT_ID}`);
// Create database
console.log('\n🗄 Creating database...');
try {
await databases.create(DB_ID, DB_NAME);
console.log(' ✓ Database created');
} catch (err) {
if (err.code === 409) {
console.log(' ⚠ Database already exists, continuing...');
} else {
throw err;
}
}
// ── balance_sheets ────────────────────────────────────────────────────────
await createCollection(
'balance_sheets',
'Balance Sheets',
[
{ type: 'integer', key: 'month', required: true },
{ type: 'integer', key: 'year', required: true },
{ type: 'string', key: 'buffer_type', required: true, size: 20 },
{ type: 'float', key: 'buffer_value', required: true, default: 0 },
{ type: 'string', key: 'user_id', required: true, size: 64 },
],
[
{ key: 'user_month_year', attributes: ['user_id', 'month', 'year'], orders: ['ASC', 'DESC', 'DESC'] },
],
);
// ── incomes ───────────────────────────────────────────────────────────────
await createCollection(
'incomes',
'Incomes',
[
{ type: 'string', key: 'balance_sheet_id', required: true, size: 64 },
{ type: 'string', key: 'name', required: true, size: 128 },
{ type: 'float', key: 'amount', required: true },
{ type: 'string', key: 'frequency', required: true, size: 20 },
{ type: 'string', key: 'user_id', required: true, size: 64 },
],
[
{ key: 'user_sheet', attributes: ['user_id', 'balance_sheet_id'] },
],
);
// ── buckets ───────────────────────────────────────────────────────────────
await createCollection(
'buckets',
'Buckets',
[
{ type: 'string', key: 'name', required: true, size: 128 },
{ type: 'string', key: 'description', required: false, size: 512, default: '' },
{ type: 'string', key: 'type', required: true, size: 32 },
{ type: 'float', key: 'current_balance', required: true, default: 0 },
{ type: 'float', key: 'goal_amount', required: false, default: 0 },
{ type: 'string', key: 'goal_type', required: false, size: 20, default: 'amount' },
{ type: 'string', key: 'goal_frequency', required: false, size: 20, default: 'monthly' },
{ type: 'float', key: 'return_percent', required: false, default: 0 },
{ type: 'string', key: 'return_frequency', required: false, size: 20, default: 'yearly' },
{ type: 'string', key: 'color', required: true, size: 16 },
{ type: 'integer', key: 'sort_order', required: false, default: 0 },
{ type: 'string', key: 'user_id', required: true, size: 64 },
],
[
{ key: 'user_order', attributes: ['user_id', 'sort_order'], orders: ['ASC', 'ASC'] },
],
);
// ── debts ─────────────────────────────────────────────────────────────────
await createCollection(
'debts',
'Debts',
[
{ type: 'string', key: 'name', required: true, size: 128 },
{ type: 'float', key: 'principal', required: true },
{ type: 'float', key: 'remaining_balance', required: true },
{ type: 'float', key: 'interest_rate', required: true },
{ type: 'string', key: 'interest_frequency', required: true, size: 20 },
{ type: 'integer', key: 'term_months', required: true },
{ type: 'float', key: 'monthly_payment', required: true },
{ type: 'boolean', key: 'is_auto_calculated', required: true, default: false },
{ type: 'string', key: 'start_date', required: true, size: 24 },
{ type: 'string', key: 'user_id', required: true, size: 64 },
],
[
{ key: 'user_id_idx', attributes: ['user_id'] },
],
);
// ── transactions ──────────────────────────────────────────────────────────
await createCollection(
'transactions',
'Transactions',
[
{ type: 'string', key: 'bucket_id', required: true, size: 64 },
{ type: 'string', key: 'type', required: true, size: 20 },
{ type: 'float', key: 'amount', required: true },
{ type: 'string', key: 'date', required: true, size: 24 },
{ type: 'string', key: 'notes', required: false, size: 512, default: '' },
{ type: 'float', key: 'balance_after', required: true },
{ type: 'string', key: 'user_id', required: true, size: 64 },
],
[
{ key: 'user_bucket_date', attributes: ['user_id', 'bucket_id', 'date'], orders: ['ASC', 'ASC', 'DESC'] },
],
);
console.log('\n\n✅ Setup complete!');
console.log('\nNext steps:');
console.log(' 1. npm run build');
console.log(' 2. Deploy the dist/ folder to your web server');
console.log(' 3. Open the app in Chrome on Android → "Add to Home Screen"');
}
main().catch((err) => {
console.error('\n❌ Setup failed:', err.message ?? err);
process.exit(1);
});