Architecture

Tech stack, project structure, and data flow.

Tech Stack

  • Framework — Next.js 14+ (App Router)
  • Language — TypeScript
  • Database — PostgreSQL via Prisma ORM (Neon serverless)
  • Auth — NextAuth.js v4 with JWT sessions + optional MFA
  • Payments — Stripe (subscriptions, embedded checkout)
  • Banking — Plaid (account linking, transaction sync)
  • Styling — Tailwind CSS with custom design tokens
  • Charts — Recharts
  • Email — Custom email templates via the email service
  • Market Data — Yahoo Finance API (server-proxied)

Project Structure

app/
  (auth)/         # Login, signup, MFA, password reset
  (dashboard)/    # Authenticated app pages
  admin/          # Admin panel (separate auth)
  knowledgebase/  # This knowledgebase (public)
  api/            # All API routes
  blog/           # Public blog

components/
  app/            # Feature-specific components
  ui/             # Reusable UI primitives

lib/              # Shared utilities, services, configs
prisma/           # Schema, migrations, seeds
scripts/          # Dev/debug scripts

Data Flow

Authentication

NextAuth handles login/signup with JWT sessions. Middleware protects dashboard routes and checks MFA status. Admin uses a separate cookie-based token.

Plaid Integration

Link Token → Plaid Link UI → Exchange Token → Store access token → Cursor-based transaction sync. All Plaid calls are server-side. Cron jobs keep data fresh.

Tier Permissions

Every API route resolves the user's subscription tier via Stripe and checks feature access against a static tier config. Client-side gating shows upgrade prompts.

Subdomain Routing

Middleware detects subdomains (admin.*, knowledgebase.*) and rewrites to the appropriate route group. This allows separate layouts and auth for each context.

Key Patterns

  • App Data ProvideruseAppData() hook provides cached user settings, tier status, and family data across all dashboard pages.
  • Error HandlingwithErrorHandler wrapper on API routes for consistent error logging and responses.
  • Tier Config — Static feature config in lib/tier-config.ts defines what each tier can access.
  • View Density — User-selectable compact/comfortable/spacious layouts via context provider.

Security Model

Required Environment Variables

  • NEXTAUTH_SECRET — Signs user session JWTs. Must be >20 characters and not the placeholder string. Generate: openssl rand -base64 32
  • ADMIN_PASSWORD_HASH — bcrypt hash of the admin password. Never set a plaintext ADMIN_PASSWORD.
  • ENCRYPTION_KEY — AES-256-GCM key for Plaid tokens at rest. Must be exactly 64 hex characters and cannot be all zeros. Generate: openssl rand -hex 32
  • MFA_ENCRYPTION_KEY — AES-256-GCM key for MFA secrets at rest. Same format as ENCRYPTION_KEY.

The server will throw at startup if NEXTAUTH_SECRET or ENCRYPTION_KEY are missing or use insecure placeholder values.

Auth & Session

  • User sessions use NextAuth JWT strategy; MFA completion is tracked via an httpOnly HMAC-SHA256 signed cookie (not the raw user ID).
  • Family child dashboard tokens are stored as SHA-256 hashes; raw tokens are shown only once at creation/rotation.

Input Validation

  • All mutation endpoints validate with Zod schemas before touching the database.
  • Budget creation enforces category ownership — category IDs from other users are rejected.
  • File uploads are validated by binary magic bytes, not client-supplied MIME type.
  • Profile image URLs are restricted to allowed CDN hostnames (res.cloudinary.com).
  • Invoice PUT is schema-validated — only editable fields (dates, items, notes) are accepted; system fields like userId, status, and customerToken cannot be overwritten.

Rate Limiting

Sensitive endpoints are rate-limited per IP. On Vercel the x-real-ip header (set by Vercel's infrastructure and not spoofable by clients) is authoritative; x-forwarded-for is used as a fallback for other reverse proxies.

The unauthenticated error-report endpoint is also rate-limited (10 req/min/IP) to prevent database abuse.

Note: The current in-memory limiter is shared only within a single serverless function instance. For stricter enforcement across concurrent instances, swap the store in lib/rate-limit.ts for an Upstash Redis counter.