Three weeks. That is the window between an empty repository and a production client portal serving real users. Not a prototype. Not a proof of concept. A fully operational, multi-tenant platform with authentication, role-based access, AI-assisted workflows, 25+ database migrations, and over 1,100 passing test cases. This is the story of how that build happened, the technical decisions that made it possible, and the numbers that prove it shipped.
The project requirements were substantial: a client portal where multiple tenants operate in complete isolation, each with their own projects, deliverables, and AI-generated content. Administrators manage all tenants from a unified interface. Clients see only their own data. The AI layer assists with onboarding, content generation, and editorial workflows. Every interaction is tracked, every deliverable is versioned, and every deployment passes an automated gate before reaching production.
Building this in 21 days was not an exercise in cutting corners. It was an exercise in eliminating waste through disciplined engineering practices: schema-first design, reusable architectural patterns, AI-governed CI, and a zero-tolerance deploy gate. Here is how each week unfolded.
Week 1: Schema, Auth, and Multi-Tenant Foundation
Day one did not start with UI mockups or component libraries. It started with a database schema. The schema-first approach means every table, column, relationship, and constraint is designed before any application code exists. This inverts the typical build order where developers create tables reactively as features demand them, resulting in inconsistent naming, missing constraints, and structural debt that compounds through the entire project lifecycle.
Database Schema Design (Days 1 and 2)
The initial schema covered 12 core tables with full referential integrity. Every table included tenant_id as a partition key, enabling Row Level Security (RLS) policies that enforce tenant isolation at the database level rather than the application level. This is a critical architectural decision: if tenant isolation is enforced in application code, a single missing WHERE clause leaks data across tenants. If it is enforced in the database, the leak is structurally impossible.
-- RLS policy: tenants can only access their own data
-- Applied to every table in the schema
CREATE POLICY "tenant_isolation" ON projects
FOR ALL
USING (tenant_id = auth.jwt() ->> 'tenant_id')
WITH CHECK (tenant_id = auth.jwt() ->> 'tenant_id');
-- Admin bypass: service role key skips RLS
-- Used only for admin operations through a dedicated Supabase client
-- This pattern repeats across all 12 core tables.
-- One policy definition. Universal enforcement. Zero application-level checks needed.
The schema also established a deliverable tracking system with versioned content. Each deliverable stores its content, status, version number, and the AI session that produced it. This creates a complete audit trail: for any piece of content in the system, you can trace back to the exact AI interaction, the exact prompt, and the exact user approval that produced it.
Authentication System (Days 2 and 3)
Authentication used Supabase Auth with custom claims for role-based access control. Three roles were defined: admin (full system access), client (tenant-scoped access), and viewer (read-only tenant access). The auth system was built on top of Supabase's JWT infrastructure, with custom claims injected during sign-up that encode the user's role and tenant assignment directly into the token.
// Role hierarchy enforced at the middleware level
// Every API route checks role before processing
type UserRole = 'admin' | 'client' | 'viewer';
const ROLE_HIERARCHY: Record<UserRole, number> = {
admin: 3,
client: 2,
viewer: 1,
};
function requireRole(minimum: UserRole) {
return (req: NextRequest) => {
const userRole = extractRole(req);
if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[minimum]) {
return NextResponse.json(
{ error: 'Insufficient permissions' },
{ status: 403 }
);
}
};
}
A critical design decision: the admin client uses the Supabase service role key, which bypasses RLS entirely. This means admin operations (viewing all tenants, managing users, running reports) are handled by a separate Supabase client instance that is never exposed to the browser. The client-facing Supabase instance uses the anon key and is always subject to RLS. Two clients, two permission models, zero ambiguity about which code path has which level of access.
Migration Strategy (Days 3 through 5)
By the end of week one, the project had 8 database migrations. Each migration is a numbered SQL file that runs in sequence, is idempotent, and includes both the forward migration and a documented rollback strategy. The migration numbering convention (001, 002, etc.) ensures deterministic ordering. Each migration does exactly one thing: create a table, add a column, create a policy, or modify a constraint. Small, atomic migrations are easier to review, easier to debug, and easier to rollback than monolithic schema changes.
By the end of the project, the migration count reached 25+. That number reflects a build that evolved through real usage and real feedback rather than one that attempted to predict every requirement upfront.
Schema-first is not about being slow and deliberate. It is about front-loading the decisions that are most expensive to change later. A column rename on day 3 costs five minutes. A column rename on day 18 costs hours of migration writing, test updating, and regression verification.
Week 2: Core Features and AI Integration
With the foundation verified (auth working, RLS enforced, migrations stable), week two focused on the features that clients actually interact with: the dashboard, project management, deliverable tracking, and the AI-assisted workflows that differentiate this portal from a generic CRUD application.
Dashboard and Project Management (Days 6 through 9)
The dashboard displays a tenant's active projects, recent deliverables, and system notifications. Each project contains phases, and each phase contains deliverables. The data model is hierarchical: tenant > project > phase > deliverable. Navigation follows the same hierarchy, and every view enforces the same tenant isolation through RLS.
Project management includes status tracking across a defined lifecycle. Projects move through phases (concept, architecture, drafting, review, production), and each phase transition requires explicit approval. This mirrors the gate model used in external delivery: the portal itself practices what it preaches. A project cannot advance to the next phase until the current phase's deliverables are approved.
AI-Assisted Workflows (Days 8 through 12)
Six AI features shipped in the portal, each following the same architectural pattern: persist the user's input to the database, then stream the AI response via Server-Sent Events (SSE). This "persist-before-stream" pattern ensures that if the AI stream fails midway, the user's input is never lost. The session can be resumed from the last persisted state.
// SSE streaming pattern used across all 6 AI features
export async function POST(req: NextRequest) {
const { projectId, input } = await req.json();
// Step 1: Persist user input BEFORE starting AI stream
const session = await db.aiSessions.create({
projectId,
input,
status: 'pending',
});
// Step 2: Create SSE stream
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
for await (const chunk of generateAIResponse(session)) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)
);
// Step 3: Persist progress incrementally
await db.aiSessions.update(session.id, {
partialOutput: chunk.accumulated,
status: 'streaming',
});
}
// Step 4: Mark complete
await db.aiSessions.update(session.id, { status: 'complete' });
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}
The AI features include: onboarding interview (structured intake that converts conversation into project specifications), content generation (produces deliverable drafts based on project context), editorial review (analyzes drafts for structural issues), revision assistant (applies specific feedback to existing content), architecture planning (generates project structure recommendations), and a context-aware chat interface for ad hoc questions about the project.
Each AI feature uses a dedicated system prompt that functions as a style anchor. The system prompt defines not just what the AI should do but how it should write: tone, vocabulary constraints, structural patterns, and explicit anti-patterns to avoid. This is not prompt engineering as decoration. It is the mechanism that ensures consistent output quality across hundreds of interactions.
The 45-Second Timeout Problem
Serverless environments impose hard timeout limits on function execution. For this deployment, the ceiling was 45 seconds. AI generation routinely exceeds that window, especially for longer deliverables. The solution: a server-side continuation loop. If the AI has not finished generating within the timeout window, the server persists the partial output, marks the session as "continuable," and the client automatically reconnects to resume generation from where it stopped. This loop can run up to 5 rounds, giving the AI effectively 225 seconds of generation time while never exceeding the 45-second function limit.
From the user's perspective, the stream is seamless. The reconnection happens in the background. The content continues flowing. The technical constraint is invisible.
Week 3: Hardening, Testing, and Production Readiness
Week three is where most rapid builds fall apart. The features work in happy-path demos, but edge cases, error states, performance under load, and security boundaries have not been tested. This is the week that separates a demo from a product.
Test Suite: 1,100+ Cases (Days 15 through 18)
The test suite covers three layers: unit tests for pure functions (prompt builders, validators, scoring algorithms, routing logic), integration tests for API routes (request validation, error responses, response shape, auth enforcement), and end-to-end tests for critical user flows (sign up, create project, generate deliverable, approve content, export).
// Test structure: organized by domain, not by type
// Each domain folder contains unit, integration, and e2e tests
tests/
auth/
unit/ -- token validation, role parsing, claim extraction
integration/ -- sign-up flow, sign-in flow, role enforcement
e2e/ -- full auth journey from landing to dashboard
projects/
unit/ -- status transitions, validation rules
integration/ -- CRUD operations, tenant isolation
e2e/ -- create project, advance phases, approve deliverables
ai/
unit/ -- prompt builders, response parsers, token counting
integration/ -- SSE streaming, session persistence, continuation
e2e/ -- full generation flow with approval
governance/
unit/ -- invariant checks, rule evaluation
integration/ -- deploy gate, CI pipeline
The test count (1,100+) is not a vanity metric. It reflects coverage of the actual surface area. Every API endpoint has at least three tests: valid input produces the expected response, invalid input produces the correct error, and unauthorized access is rejected. Every database operation is tested for tenant isolation: a query made with Tenant A's credentials must never return Tenant B's data. Every AI feature is tested for session persistence: if the stream is interrupted at any point, the session state is recoverable.
Security Audit (Days 17 and 18)
The security audit was not a checkbox exercise. It covered five areas:
- RLS verification: Every table's RLS policy was tested by attempting cross-tenant access with valid credentials for a different tenant. Every attempt must fail.
- Auth boundary testing: Every API route was called without authentication, with expired tokens, with tokens for the wrong role, and with tokens for the wrong tenant. Every unauthorized request must return 401 or 403.
- Input validation: Every user-facing input field was tested with malformed data, oversized payloads, SQL injection patterns, and XSS payloads. The validation layer (Zod schemas) must reject all malformed input before it reaches the database.
- Admin client isolation: The service role client (which bypasses RLS) must never be accessible from browser-side code. This was verified by auditing every import path to ensure the admin client is only imported in server-side modules.
- Error response sanitization: Error responses must never expose internal state, stack traces, database column names, or query details. Every error handler was verified to return generic messages with correlation IDs for internal debugging.
Deploy Pipeline (Days 19 through 21)
The deploy pipeline runs as a GitHub Actions workflow with six stages: lint, typecheck, test, governance checks, backup verification, and end-to-end tests. Every stage must pass. A failure at any stage blocks the deployment. There is no manual override, no "skip CI" flag, no escape hatch.
# Simplified CI pipeline (GitHub Actions)
name: Deploy Gate
on:
push:
branches: [main]
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Unit + Integration Tests
run: npm run test
- name: Governance Invariants
run: npm run governance:check
- name: Backup Verification
run: npm run backup:verify
- name: E2E Tests
run: npx playwright test
- name: Deploy to Production
if: success()
run: npx vercel --prod
The governance check stage deserves specific attention. This step runs a set of project-specific invariants that go beyond standard linting. Examples: no em dashes in any prose-generating code (style enforcement), no direct database queries outside the data access layer (architectural enforcement), no client-side imports of the admin Supabase client (security enforcement). These rules are codified as automated checks, not documented guidelines. They cannot be accidentally violated because the CI pipeline will catch them before the code reaches production.
The final deploy on day 21 was unremarkable. No drama. No late-night emergency fixes. No "let's just push it and see." The deploy gate passed on the first attempt because every preceding gate had already verified the work. That lack of drama is the point.
The Technical Decisions That Made 21 Days Possible
Speed at this level is not about working longer hours or writing code faster. It is about eliminating decisions that do not need to be made and reusing patterns that have already been proven.
Why Next.js + Supabase: This stack eliminates an entire category of infrastructure decisions. Authentication, database, real-time subscriptions, file storage, and edge functions are all provided by Supabase. Server-side rendering, API routes, middleware, and static optimization are all provided by Next.js. The team spends zero time configuring infrastructure and all of its time building features.
Why RLS for tenant isolation: Application-level tenant filtering is a single point of failure. One missed WHERE tenant_id = ? clause, one raw query, one ORM misconfiguration, and data leaks across tenants. RLS policies are defined once per table and enforced universally. The database becomes the security boundary, not the application code.
Why SSE for AI streaming: WebSockets add operational complexity (connection management, reconnection logic, state synchronization). SSE provides unidirectional streaming over standard HTTP, works through CDNs and proxies without special configuration, and degrades gracefully if the connection drops. For AI generation where data flows in one direction (server to client), SSE is the simpler, more reliable choice.
Why schema-first: The schema is the contract between every layer of the application. When the schema is designed first, API routes, validation schemas, TypeScript types, and test fixtures can all be derived from a single source of truth. Changes propagate predictably. Inconsistencies between layers are structurally impossible because every layer references the same schema definition.
The Numbers
Metrics tell the story that narrative cannot. Here is what the build produced in 21 calendar days:
- 25+ database migrations: Each atomic, each reversible, each tested independently.
- 1,100+ test cases: Unit, integration, and end-to-end. Every API endpoint. Every auth boundary. Every tenant isolation policy.
- 6 AI-assisted features: Onboarding, content generation, editorial review, revision assistant, architecture planning, contextual chat.
- Multi-tenant isolation: RLS policies on every table, verified by cross-tenant access testing.
- Automated deploy gate: 7 checks, all required, no manual override.
- Zero post-launch critical bugs: The hardening pass caught every edge case before production.
These numbers are not aspirational targets or marketing claims. They are artifacts in the repository: migration files you can count, test files you can run, deploy logs you can audit. "With receipts" is not a tagline. It is the engineering culture.
Twenty-one days is not a shortcut. It is what happens when you remove the waste: the undocumented decisions, the ad hoc architecture, the manual testing, the "ship and pray" deploys. What remains is pure execution against a verified plan.