PromptBase
Upgrade
Close icon
General
Home
Marketplace
Create
Hire
Login
Chat
Sell
Explore

Prompt Details

Model
(claude-4-6-sonnet)
Token size
586
Example input
[APP_DESCRIPTION]: A multi-tenant SaaS invoicing platform with subscription billing, PDF export, and client portal [TECH_STACK]: Next.js, Node.js, PostgreSQL, Stripe, Prisma, Tailwind CSS [APP_TYPE]: Web SaaS [LANGUAGE]: TypeScript [SCALE]: 0–10,000 users, single region
Example output
Here is the complete production-ready blueprint for your multi-tenant SaaS invoicing platform. Multi-Tenant SaaS Invoicing Platform β€” Architecture Blueprint 1. Project Overview Purpose: A cloud-based invoicing platform that lets businesses (tenants) manage clients, create invoices, collect payments via Stripe, and give clients a self-service portal β€” all under subscription billing tiers. Core Features: Multi-tenant workspace isolation (org-scoped data, subdomain routing) Invoice lifecycle management (draft β†’ sent β†’ paid β†’ overdue) Stripe Subscription billing with plan enforcement (Free / Pro / Business) PDF invoice generation with branded templates Client portal (view invoices, pay online, download PDFs) Recurring invoice scheduling Revenue dashboard (MRR, outstanding, aging report) Webhook-driven payment status sync from Stripe Target Users: Freelancers, small agencies, and SMBs needing structured invoicing without accounting software complexity. Architectural Pattern: Modular Monolith with service-oriented feature modules At 0–10k users in a single region, a full microservices split introduces operational overhead with no tangible benefit. A modular monolith β€” strict module boundaries enforced at the code level, single deployment unit β€” delivers clean separation of concerns (Auth, Billing, Invoicing, Portal, PDF) while remaining easy to reason about, deploy, and debug. Feature modules communicate through well-typed internal APIs, not network calls. If a specific module (e.g. PDF generation) needs extraction later, the boundary already exists. 2. Full Folder Structure invoicely/ β”œβ”€β”€ .github/ β”‚ └── workflows/ β”‚ β”œβ”€β”€ ci.yml # lint, type-check, test on PR β”‚ └── deploy.yml # deploy to production on main merge β”‚ β”œβ”€β”€ apps/ β”‚ └── web/ # Next.js application (App Router) β”‚ β”œβ”€β”€ app/ β”‚ β”‚ β”œβ”€β”€ (auth)/ # unauthenticated routes group β”‚ β”‚ β”‚ β”œβ”€β”€ login/ β”‚ β”‚ β”‚ β”‚ └── page.tsx # login page β”‚ β”‚ β”‚ β”œβ”€β”€ register/ β”‚ β”‚ β”‚ β”‚ └── page.tsx # org signup page β”‚ β”‚ β”‚ └── layout.tsx # auth shell layout β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ (dashboard)/ # authenticated tenant routes group β”‚ β”‚ β”‚ β”œβ”€β”€ layout.tsx # sidebar + top-nav shell β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # revenue dashboard β”‚ β”‚ β”‚ β”œβ”€β”€ invoices/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # invoice list β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ new/ β”‚ β”‚ β”‚ β”‚ β”‚ └── page.tsx # invoice builder β”‚ β”‚ β”‚ β”‚ └── [id]/ β”‚ β”‚ β”‚ β”‚ └── page.tsx # invoice detail / edit β”‚ β”‚ β”‚ β”œβ”€β”€ clients/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # client list β”‚ β”‚ β”‚ β”‚ └── [id]/ β”‚ β”‚ β”‚ β”‚ └── page.tsx # client detail + invoice history β”‚ β”‚ β”‚ β”œβ”€β”€ settings/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # org branding, email settings β”‚ β”‚ β”‚ β”‚ └── billing/ β”‚ β”‚ β”‚ β”‚ └── page.tsx # subscription plan management β”‚ β”‚ β”‚ └── reports/ β”‚ β”‚ β”‚ └── page.tsx # aging report, MRR chart β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ portal/ # client-facing portal (public) β”‚ β”‚ β”‚ └── [token]/ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # invoice view + pay button β”‚ β”‚ β”‚ └── success/ β”‚ β”‚ β”‚ └── page.tsx # payment confirmation β”‚ β”‚ β”‚ β”‚ β”‚ └── api/ # Next.js Route Handlers β”‚ β”‚ β”œβ”€β”€ auth/ β”‚ β”‚ β”‚ └── [...nextauth]/ β”‚ β”‚ β”‚ └── route.ts # NextAuth.js handler β”‚ β”‚ β”œβ”€β”€ webhooks/ β”‚ β”‚ β”‚ └── stripe/ β”‚ β”‚ β”‚ └── route.ts # Stripe event ingestion β”‚ β”‚ β”œβ”€β”€ invoices/ β”‚ β”‚ β”‚ β”œβ”€β”€ route.ts # GET list, POST create β”‚ β”‚ β”‚ └── [id]/ β”‚ β”‚ β”‚ β”œβ”€β”€ route.ts # GET, PATCH, DELETE β”‚ β”‚ β”‚ β”œβ”€β”€ send/ β”‚ β”‚ β”‚ β”‚ └── route.ts # POST: email invoice to client β”‚ β”‚ β”‚ └── pdf/ β”‚ β”‚ β”‚ └── route.ts # GET: generate + stream PDF β”‚ β”‚ β”œβ”€β”€ clients/ β”‚ β”‚ β”‚ β”œβ”€β”€ route.ts # GET list, POST create β”‚ β”‚ β”‚ └── [id]/ β”‚ β”‚ β”‚ └── route.ts # GET, PATCH, DELETE β”‚ β”‚ β”œβ”€β”€ portal/ β”‚ β”‚ β”‚ └── [token]/ β”‚ β”‚ β”‚ β”œβ”€β”€ route.ts # GET invoice by public token β”‚ β”‚ β”‚ └── pay/ β”‚ β”‚ β”‚ └── route.ts # POST: create Stripe Payment Intent β”‚ β”‚ └── billing/ β”‚ β”‚ β”œβ”€β”€ checkout/ β”‚ β”‚ β”‚ └── route.ts # POST: create Stripe Checkout Session β”‚ β”‚ └── portal/ β”‚ β”‚ └── route.ts # POST: create Stripe Billing Portal session β”‚ β”‚ β”‚ β”œβ”€β”€ components/ # React UI components β”‚ β”‚ β”œβ”€β”€ ui/ # shadcn/ui primitives β”‚ β”‚ β”‚ β”œβ”€β”€ button.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ dialog.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ table.tsx β”‚ β”‚ β”‚ └── ... β”‚ β”‚ β”œβ”€β”€ invoices/ β”‚ β”‚ β”‚ β”œβ”€β”€ InvoiceForm.tsx # line-item builder form β”‚ β”‚ β”‚ β”œβ”€β”€ InvoiceTable.tsx # sortable invoice list β”‚ β”‚ β”‚ └── StatusBadge.tsx # visual status indicator β”‚ β”‚ β”œβ”€β”€ clients/ β”‚ β”‚ β”‚ └── ClientCard.tsx β”‚ β”‚ β”œβ”€β”€ dashboard/ β”‚ β”‚ β”‚ β”œβ”€β”€ RevenueChart.tsx # recharts MRR chart β”‚ β”‚ β”‚ └── StatCard.tsx # KPI summary card β”‚ β”‚ └── layout/ β”‚ β”‚ β”œβ”€β”€ Sidebar.tsx β”‚ β”‚ └── TopNav.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ lib/ # app-level utilities β”‚ β”‚ β”œβ”€β”€ auth.ts # NextAuth config + session helpers β”‚ β”‚ β”œβ”€β”€ prisma.ts # singleton Prisma client β”‚ β”‚ β”œβ”€β”€ stripe.ts # Stripe SDK singleton β”‚ β”‚ β”œβ”€β”€ pdf.ts # puppeteer/Playwright PDF wrapper β”‚ β”‚ β”œβ”€β”€ email.ts # Resend email client β”‚ β”‚ └── tenant.ts # tenant resolution from session/header β”‚ β”‚ β”‚ └── middleware.ts # org auth guard + route protection β”‚ β”œβ”€β”€ packages/ β”‚ β”œβ”€β”€ db/ # Prisma schema + migrations (shared) β”‚ β”‚ β”œβ”€β”€ prisma/ β”‚ β”‚ β”‚ β”œβ”€β”€ schema.prisma # all data models β”‚ β”‚ β”‚ └── migrations/ # migration history β”‚ β”‚ β”œβ”€β”€ seed.ts # dev seed data β”‚ β”‚ └── index.ts # re-exports Prisma client type β”‚ β”‚ β”‚ β”œβ”€β”€ pdf-templates/ # invoice HTML templates for PDF rendering β”‚ β”‚ β”œβ”€β”€ default/ β”‚ β”‚ β”‚ └── template.tsx # React component β†’ HTML string β”‚ β”‚ └── minimal/ β”‚ β”‚ └── template.tsx β”‚ β”‚ β”‚ └── types/ # shared TypeScript domain types β”‚ β”œβ”€β”€ invoice.ts β”‚ β”œβ”€β”€ tenant.ts β”‚ └── billing.ts β”‚ β”œβ”€β”€ scripts/ β”‚ β”œβ”€β”€ seed-dev.ts # populate local DB with fixtures β”‚ └── migrate-prod.sh # safe production migration wrapper β”‚ β”œβ”€β”€ tests/ β”‚ β”œβ”€β”€ unit/ # Jest unit tests (pure functions) β”‚ β”‚ β”œβ”€β”€ invoice-calculations.test.ts β”‚ β”‚ └── tenant-resolver.test.ts β”‚ β”œβ”€β”€ integration/ # API route tests (test DB) β”‚ β”‚ β”œβ”€β”€ invoices.test.ts β”‚ β”‚ └── webhooks-stripe.test.ts β”‚ └── e2e/ # Playwright browser tests β”‚ β”œβ”€β”€ invoice-flow.spec.ts β”‚ └── client-portal.spec.ts β”‚ β”œβ”€β”€ .env.example # documented env var template β”œβ”€β”€ .env.local # local secrets (gitignored) β”œβ”€β”€ docker-compose.yml # local PostgreSQL + Redis β”œβ”€β”€ turbo.json # Turborepo pipeline config β”œβ”€β”€ package.json # root workspace config β”œβ”€β”€ tsconfig.base.json # shared TS config └── prettier.config.js # formatting config 3. Layer-by-Layer Breakdown Routing & Middleware Layer (apps/web/middleware.ts, route groups) Next.js App Router route groups enforce authentication boundaries without prop drilling. The middleware intercepts every request, resolves the tenant from the session JWT, and attaches it to request headers. Public routes (/portal/[token], /login, /register, /api/webhooks/stripe) bypass auth. All (dashboard) routes receive a guaranteed orgId context. API Route Handlers (apps/web/app/api/) Thin orchestration layer. Each handler: validates the incoming request with Zod, calls the appropriate service function from the module layer, and returns a typed JSON response. No business logic lives here. The Stripe webhook handler (/api/webhooks/stripe) validates the raw body signature before parsing β€” raw body buffering is critical and handled explicitly. Service / Module Layer (apps/web/lib/) Domain logic lives here, organised by concern: auth.ts β€” session management, org membership checks tenant.ts β€” resolves orgId from session, enforces plan limits (e.g. invoice count ceiling on Free tier) pdf.ts β€” calls Puppeteer with the HTML from packages/pdf-templates, streams the buffer back email.ts β€” wraps Resend SDK, renders portal link into email body stripe.ts β€” exposes typed wrappers for Checkout, PaymentIntent, and BillingPortal creation Data Layer (packages/db/) Prisma is the single source of truth. Schema changes always go through migration files (never db push in production). The Prisma client is instantiated as a singleton in apps/web/lib/prisma.ts to avoid connection pool exhaustion under Next.js hot-reload. Row-level data isolation is enforced by always scoping queries with where: { orgId }. PDF Template Layer (packages/pdf-templates/) React components rendered to static HTML strings via renderToStaticMarkup, then hydrated into Puppeteer. Keeping templates in a separate package means they can be independently tested and swapped without touching route logic. Client Portal Layer (app/portal/[token]/) Entirely stateless from an auth perspective. Access is governed by a cryptographically random UUID token stored on the Invoice record (portalToken). No session or JWT is required β€” the token IS the credential. Expiry and revocation live on the Invoice model. 4. Data Models & Relationships Organization (tenant) β”œβ”€β”€ id, name, slug, logoUrl β”œβ”€β”€ stripeCustomerId, stripeSubscriptionId β”œβ”€β”€ plan: ENUM(FREE, PRO, BUSINESS) β”œβ”€β”€ planStatus: ENUM(ACTIVE, PAST_DUE, CANCELED) └── createdAt User β”œβ”€β”€ id, email, name, passwordHash β”œβ”€β”€ orgId β†’ Organization (many users per org) β”œβ”€β”€ role: ENUM(OWNER, ADMIN, MEMBER) └── createdAt Client β”œβ”€β”€ id, name, email, phone β”œβ”€β”€ address, city, country, taxId β”œβ”€β”€ orgId β†’ Organization └── createdAt Invoice β”œβ”€β”€ id, invoiceNumber (org-scoped sequence) β”œβ”€β”€ orgId β†’ Organization β”œβ”€β”€ clientId β†’ Client β”œβ”€β”€ status: ENUM(DRAFT, SENT, PAID, VOID, OVERDUE) β”œβ”€β”€ issueDate, dueDate β”œβ”€β”€ currency, subtotal, taxRate, taxAmount, total β”œβ”€β”€ notes, terms β”œβ”€β”€ portalToken (UUID, unique) ← public access credential β”œβ”€β”€ stripePaymentIntentId (nullable) └── paidAt (nullable) InvoiceLineItem β”œβ”€β”€ id, invoiceId β†’ Invoice β”œβ”€β”€ description, quantity, unitPrice, amount └── sortOrder RecurringSchedule β”œβ”€β”€ id, orgId β†’ Organization β”œβ”€β”€ clientId β†’ Client β”œβ”€β”€ frequency: ENUM(WEEKLY, MONTHLY, QUARTERLY, ANNUALLY) β”œβ”€β”€ nextRunAt, lastRunAt β”œβ”€β”€ templateSnapshot: JSON ← frozen line-item snapshot └── isActive Payment β”œβ”€β”€ id, invoiceId β†’ Invoice β”œβ”€β”€ stripePaymentIntentId, amount, currency β”œβ”€β”€ status: ENUM(PENDING, SUCCEEDED, FAILED, REFUNDED) └── paidAt AuditLog β”œβ”€β”€ id, orgId, userId, action β”œβ”€β”€ resourceType, resourceId β”œβ”€β”€ metadata: JSON └── createdAt Key relationships: Organization 1:N Users, Clients, Invoices, RecurringSchedules Invoice 1:N InvoiceLineItems, Payments Client 1:N Invoices RecurringSchedule generates Invoices on cron trigger 5. API / Interface Contracts MethodPathInputOutputAuthPOST/api/auth/[...nextauth]{email, password}Session cookiePublicGET/api/invoices?status&clientId&page{invoices[], total}Session + orgIdPOST/api/invoices{clientId, lineItems[], dueDate, currency, notes}InvoiceSession + orgIdGET/api/invoices/[id]β€”Invoice + lineItems[]Session + orgIdPATCH/api/invoices/[id]Partial Invoice fieldsInvoiceSession + orgIdDELETE/api/invoices/[id]β€”204Session + orgId (OWNER/ADMIN)POST/api/invoices/[id]/send{message?}{sent: true}Session + orgIdGET/api/invoices/[id]/pdfβ€”PDF stream (application/pdf)Session + orgIdGET/api/clients?search&page{clients[], total}Session + orgIdPOST/api/clients{name, email, address, ...}ClientSession + orgIdPATCH/api/clients/[id]Partial Client fieldsClientSession + orgIdDELETE/api/clients/[id]β€”204Session + orgIdGET/api/portal/[token]β€”{invoice, client, org}Token (public)POST/api/portal/[token]/pay{amount, currency}{clientSecret}Token (public)POST/api/billing/checkout{planId}{checkoutUrl}Session + orgIdPOST/api/billing/portalβ€”{portalUrl}Session + orgIdPOST/api/webhooks/stripeRaw Stripe event body200 OKStripe-Signature header Webhook events handled: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed, payment_intent.succeeded 6. Key Dependencies & Rationale PackageVersion CategoryRationalenext14.x (App Router)Full-stack framework with RSC, route handlers, and middleware β€” eliminates separate Express servertypescript5.xEnd-to-end type safety from DB schema to UI components@prisma/client5.xType-safe ORM with migration tooling; schema-first approach keeps DB and types in syncprisma5.xCLI for migrations, introspection, and studio dev toolstripe14.xOfficial SDK for Checkout, PaymentIntent, webhooks, and Billing Portalnext-auth4.xSession management with JWT, credential provider, and database adaptertailwindcss3.xUtility-first CSS; zero runtime overhead, consistent design tokens@radix-ui/react-*latestAccessible, unstyled headless primitives β€” composed with Tailwind for shadcn/uizod3.xRuntime validation for all API inputs; shares types with react-hook-formreact-hook-form7.xPerformant form state management for invoice builder without re-render overhead@react-pdf/renderer3.xReact-based PDF generation without headless browser dependency (alternative to Puppeteer)resend2.xTransactional email with React email templates; deliverability-focusedrecharts2.xDeclarative React charts for revenue dashboardsdate-fns3.xTree-shakeable date utilities for due-date calculations and aging reports@tanstack/react-query5.xServer-state caching for client-side data fetching in dashboard componentsjose5.xPortal token signing and verification (lightweight JWT/JWE)turbolatestMonorepo task runner with caching for build/lint/test pipelines 7. Environment & Config Strategy bash# .env.example # ─── Database ─────────────────────────────────── DATABASE_URL="postgresql://invoicely:password@localhost:5432/invoicely_dev" # ─── NextAuth ─────────────────────────────────── NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32" # ─── Stripe ───────────────────────────────────── STRIPE_SECRET_KEY="sk_test_..." STRIPE_PUBLISHABLE_KEY="pk_test_..." STRIPE_WEBHOOK_SECRET="whsec_..." # Stripe Price IDs (from your Stripe dashboard) STRIPE_PRICE_PRO_MONTHLY="price_..." STRIPE_PRICE_PRO_YEARLY="price_..." STRIPE_PRICE_BUSINESS_MONTHLY="price_..." STRIPE_PRICE_BUSINESS_YEARLY="price_..." # ─── Email (Resend) ────────────────────────────── RESEND_API_KEY="re_..." EMAIL_FROM="billing@yourdomain.com" # ─── App ───────────────────────────────────────── NEXT_PUBLIC_APP_URL="http://localhost:3000" NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..." # ─── Portal Token Signing ──────────────────────── PORTAL_TOKEN_SECRET="generate-with-openssl-rand-base64-32" # ─── Optional: Redis (for rate limiting / job queue) ── REDIS_URL="redis://localhost:6379" Config loading strategy: Next.js loads .env.local automatically in development. Variables prefixed NEXT_PUBLIC_ are inlined at build time and exposed to the browser β€” keep only non-sensitive Stripe publishable key and app URL there. All other secrets remain server-only. In production (Vercel or Railway), environment variables are injected at the platform level. A src/lib/env.ts file uses zod to validate and parse all required env vars at startup, throwing a descriptive error if any are missing β€” this catches misconfigured deployments before they reach users: typescript// lib/env.ts import { z } from 'zod' const envSchema = z.object({ DATABASE_URL: z.string().url(), NEXTAUTH_SECRET: z.string().min(32), STRIPE_SECRET_KEY: z.string().startsWith('sk_'), STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'), RESEND_API_KEY: z.string(), PORTAL_TOKEN_SECRET: z.string().min(32), }) export const env = envSchema.parse(process.env) ``` --- ## 8. Testing Strategy **Testing Pyramid:** ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ E2E β”‚ ← Playwright (5–10 critical flows) β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Integration β”‚ ← Vitest + test DB (API routes, webhooks) β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Unit (pure logic) β”‚ ← Vitest (calculations, resolvers, validators) β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Test tools: LayerToolWhyUnitVitestFaster than Jest, native ESM, compatible with Next.jsIntegrationVitest + @prisma/client test DBReal DB queries against a test_ schema, seeded per suiteAPI mockingmsw (Mock Service Worker)Intercept Stripe/Resend calls without hitting live APIsE2EPlaywrightChromium-based, excellent Next.js support, network interceptionCoverage@vitest/coverage-v8V8-native, fast, accurate Concrete test examples: Unit β€” invoice total calculation: typescript// tests/unit/invoice-calculations.test.ts import { calculateInvoiceTotals } from '@/lib/invoice-utils' test('applies tax rate to subtotal correctly', () => { const result = calculateInvoiceTotals([ { quantity: 2, unitPrice: 150_00 }, // cents { quantity: 1, unitPrice: 80_00 }, ], { taxRate: 0.20 }) expect(result.subtotal).toBe(380_00) expect(result.taxAmount).toBe(76_00) expect(result.total).toBe(456_00) }) Integration β€” invoice creation API: typescript// tests/integration/invoices.test.ts import { createTestOrg, createTestSession } from '../helpers' test('POST /api/invoices creates invoice scoped to org', async () => { const { org, client } = await createTestOrg() const session = await createTestSession(org) const res = await fetch('/api/invoices', { method: 'POST', headers: { Cookie: session.cookie, 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: client.id, dueDate: '2025-12-31', currency: 'USD', lineItems: [{ description: 'Design work', quantity: 1, unitPrice: 500_00 }], }), }) const invoice = await res.json() expect(res.status).toBe(201) expect(invoice.orgId).toBe(org.id) expect(invoice.status).toBe('DRAFT') }) E2E β€” invoice send and portal access: typescript// tests/e2e/invoice-flow.spec.ts test('client can view and pay invoice via portal link', async ({ page }) => { await page.goto('/login') await page.fill('[name=email]', 'owner@acme.com') await page.fill('[name=password]', 'testpass') await page.click('[type=submit]') await page.goto('/invoices/new') // ... fill form, click Send // Intercept sent email, extract portal URL const portalUrl = await getLastEmailPortalLink() await page.goto(portalUrl) await expect(page.getByText('Invoice #0001')).toBeVisible() await expect(page.getByRole('button', { name: 'Pay now' })).toBeVisible() }) 9. CI/CD & Deployment Blueprint Deployment target: Vercel (app) + Neon or Supabase (PostgreSQL) β€” both offer connection pooling (PgBouncer) critical for serverless Next.js. Pipeline stages: yaml# .github/workflows/ci.yml name: CI on: pull_request: branches: [main] jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: 'npm' } - run: npm ci - run: npm run type-check # tsc --noEmit - run: npm run lint # eslint - run: npm run format:check # prettier test-unit: needs: quality runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm run test:unit # vitest run tests/unit test-integration: needs: quality runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_DB: invoicely_test POSTGRES_PASSWORD: test ports: ['5432:5432'] env: DATABASE_URL: postgresql://postgres:test@localhost:5432/invoicely_test steps: - uses: actions/checkout@v4 - run: npm ci - run: npx prisma migrate deploy - run: npm run test:integration test-e2e: needs: [test-unit, test-integration] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx playwright install --with-deps chromium - run: npm run build - run: npm run test:e2e # .github/workflows/deploy.yml on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: bash scripts/migrate-prod.sh # prisma migrate deploy on prod DB - uses: vercel/action@v1 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} vercel-args: '--prod' Environment promotion: feature/* branches β†’ Vercel Preview deployments (auto, own preview URL) main β†’ Production (gated by passing CI) Migration safety: scripts/migrate-prod.sh runs prisma migrate deploy (never migrate dev) and exits non-zero on any failure before the Vercel deploy step proceeds, preventing schema/code mismatch deploys. 10. Architect's Notes Non-obvious decisions worth flagging: 1. Stripe webhook idempotency is your most critical edge case. Stripe retries events on non-2xx responses for up to 72 hours. Without an idempotency check, a payment can be marked PAID multiple times. Store processed stripeEventId values in a deduplicated StripeEvent table and return 200 immediately for already-processed events before doing any work. 2. Multi-tenancy via orgId scoping is correct at this scale β€” but audit every Prisma query. There is no database-level row isolation (unlike PostgreSQL Row Level Security). Any forgotten where: { orgId } clause leaks cross-tenant data. Enforce this by wrapping Prisma in a factory function that injects orgId into every query: withTenant(prisma, orgId).invoice.findMany(...). 3. PDF generation is the main performance risk. @react-pdf/renderer runs synchronously in the Node.js thread. For invoices with many line items it can block for 200–400ms. Move PDF generation to a background job (BullMQ + Redis) and respond with a pre-signed S3 URL rather than streaming synchronously. At low scale this is optional; plan the interface so swapping is non-breaking. 4. Portal token security. A UUID v4 token (portalToken) is statistically secure but has no built-in expiry. Add portalTokenExpiresAt to the Invoice model and check it in the /api/portal/[token] handler. Rotating the token on each send action prevents stale forwarded links from remaining valid indefinitely. 5. Plan enforcement must be centralised. Free-tier limits (e.g. max 10 invoices/month) are trivially bypassed if checked inside individual route handlers. Extract a checkPlanLimit(orgId, feature) function called from a single middleware point, and back it with a cached count query (Redis TTL 60s) to avoid hitting the DB on every request. Concrete next steps: Bootstrap the Prisma schema and run prisma migrate dev β€” get the DB contract locked in before writing any route handlers. The data model is the foundation everything else builds on. Wire Stripe webhooks first β€” set up the webhook handler, Stripe CLI local forwarding (stripe listen --forward-to localhost:3000/api/webhooks/stripe), and the idempotency table before building any payment UI. Getting the async payment loop right early prevents painful retrofitting. Build the client portal before the dashboard β€” it has zero auth complexity and gives you a complete invoice β†’ PDF β†’ payment flow to validate your data model end-to-end in a day. Add zod validation to every API route before shipping β€” unenforced inputs are a security and stability risk; bake validation in from the first route, not as a cleanup pass. Set up Vercel Preview deployments and a seed script immediately β€” previews with realistic seed data let you share working features before production exists, and the seed script pays compounding dividends every time a developer clones the repo. ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation-duration: 0.25s; animation-timing-function: cubic-bezier(0.19, 1, 0.22, 1); } VvisualizeVvisualize show_widgetThe diagram above shows the full architecture at a glance β€” external actors at the top, Next.js app layer routing into four domain modules, Prisma as the unified data layer over PostgreSQL, and external services (Stripe, Resend, PDF renderer, Redis) wired to the modules that own them. A few things worth calling out that don't fit neatly into any single section: the portalToken on the Invoice model is doing real security work and should be treated with as much care as an API key. The orgId-scoped Prisma wrapper pattern in note #2 is probably the most important structural decision you can make before writing your first route β€” retrofitting it later is painful. And if you're targeting Vercel, plan for the serverless cold-start reality early: keep Prisma connection pooling (PgBouncer via Neon/Supabase) configured from day one, not after you see connection exhaustion in production logs.
πŸŒ€ Claude

Pro Coding Architect

Add to Cart
Instant accessInstant access
Usage rightsCommercial use
Money-back guaranteeMoney‑back
By purchasing this prompt, you agree to our terms of service
CLAUDE-4-6-SONNET
Tested icon
Guide icon
4 examples icon
Free credits icon
Blank file. Blank architecture. Hours lost planning. This prompt generates a full app blueprint β€” tailored folder structure, data models, API contracts, dependencies, and CI/CD plan β€” matched to your stack and scale. πŸ—‚οΈ Dynamic folder tree β€” fits your stack, not a generic template 🧱 Layer-by-layer breakdown with real domain names πŸ”— API contracts + data models ready to implement βš™οΈ Env config, testing strategy & deployment plan included πŸ—οΈ Works for web, mobile, CLI, desktop & microservices
...more
Added 3 weeks ago
Report
Browse Marketplace