A dashboard description is not enough. Define the database constraints, authentication model, tenant boundary, failure states, deployment target, and acceptance tests.
This guide builds SignalRoom, a customer-feedback SaaS where teams publish boards, users submit ideas and vote, and owners move requests through a public roadmap. We built the companion app, migrated it from SQLite to Supabase Postgres, deployed it on Vercel, and tested its main user flows.
The implementation snapshot, live SignalRoom demo, and passing GitHub Actions run are public.
At the pinned commit, SignalRoom passed 34 tests, desktop and Pixel 7 Playwright runs, axe checks, lint, typecheck, build, and a production dependency audit. Hosted checks covered the main feedback flow and confirmed that records remained after a fresh Vercel deployment.
This guide assumes you already chose the product. For the earlier planning work, see the SaaS MVP roadmap.
Once the product works, use the separate SaaS landing-page prompt guide to add conversion copy, lead storage, first-party analytics, metadata, legal-page structure, accessibility checks, and deployment evidence without pretending unfinished features already exist.
Use this prompt sequence if
- you are building a multi-tenant SaaS with React, Express, Supabase, and Vercel
- you want custom server-managed sessions instead of Supabase Auth
- you need public boards plus server-enforced owner, admin, and member roles
- you are prepared to test and revise generated code rather than expect a one-shot build
This is not a no-code tutorial, and the companion validates the implementation approach—not guaranteed one-click reproduction from the prompts.
What SignalRoom actually does
The current release supports:
- register, sign in, sign out, and restore a server-side session
- create a workspace and become its owner
- create a public feedback board with a unique slug
- submit feedback, vote once, remove a vote, and comment
- filter requests by roadmap status
- move requests through
under_review,planned,in_progress, andshippedwhen their membership role permits it - open a public board without signing in
The API tests cover organization membership checks on the server. In the tested owner, member, outsider, and cross-tenant scenarios, substituting organization, board, or post IDs did not grant access.
What this release deliberately leaves out
This release does not include:
- Stripe subscriptions and plan enforcement
- transactional email
- invitations
- password resets and email verification
- duplicate-post merging
- account deletion and “sign out all devices” controls
- enterprise SSO or compliance certification
Add those later. The feedback loop works without billing or email.
Stack used in the companion build
SignalRoom uses:
- React 19 and TypeScript for the interface
- Vite for development and the browser production build
- Express 5 for the JSON API
- Node 22 for local and hosted server execution
- Supabase Postgres for durable hosted data
- the
pgdriver through Supabase’s connection pooler - bcryptjs 3 for password hashing
- opaque server-side sessions stored as SHA-256 token hashes
- Vitest, Supertest, Testing Library, and PGlite for deterministic tests
- Playwright and axe-core for browser and accessibility checks
- Vercel’s Express runtime for deployment
The browser never receives a privileged Supabase credential and does not query the database directly. React calls same-origin /api routes. Express validates the session, checks membership and role rules, then sends parameterized SQL to Postgres.
Architecture before prompting
The organization is the tenant boundary:
Browser
└── same-origin /api request
└── Express function
├── session lookup
├── membership and role check
└── Supabase Postgres
├── Organization
│ └── Membership
└── Board
└── Feedback post
├── Vote
├── Comment
└── Status history
Roles live on memberships, not users. One person can own one organization and remain a regular member of another.
Database rules that matter
The schema uses UUIDs and timestamptz, with database constraints and indexes enforcing the rules below:
- unique user emails
- unique organization and board slugs
- one membership per organization and user
owner,admin, ormemberroles only- one vote per user and feedback post
- known roadmap statuses only
- deliberate cascade and restrict behavior
The PostgreSQL constraints documentation is worth keeping open while reviewing generated migrations. Application validation improves error messages, but the database still needs to reject invalid state.
Why RLS is enabled even though the browser uses Express
SignalRoom does not use Supabase Auth or direct browser-to-database queries. Express remains the trusted authorization layer. Even so, every application table has Row Level Security enabled, and the anon and authenticated database roles receive no table access.
That gives the Supabase data API a deny-by-default posture. The server’s pooled Postgres connection performs trusted queries after the application validates the user’s session and membership.
Do not copy this architecture and then add broad RLS policies just to make a failing client query work. If you decide to access Supabase directly from the browser, design and test those policies as a separate security boundary.
Prompt 1: SaaS feedback board architecture and requirements
Copy this into an AI coding agent working in an empty repository. For the reasoning behind the structure, see how to write better AI coding prompts and the examples in vibe coding prompts that work:
Build a production-minded customer-feedback SaaS named SignalRoom.
Target stack:
- React 19
- TypeScript in strict mode
- Vite
- Express 5
- Node 22
- Supabase-hosted Postgres accessed through the pg driver
- Supabase transaction-mode connection pooling for serverless requests
- bcryptjs password hashing with cost 12
- opaque server-side sessions stored as SHA-256 token hashes
- Vitest, Supertest, Testing Library, PGlite, Playwright, and axe-core
- Vercel for the static frontend and Express API function
Architecture rules:
1. The React client calls same-origin /api routes. It must not receive DATABASE_URL, a service-role key, or any privileged database credential.
2. Express owns authentication, authorization, validation, and database access.
3. Users, sessions, organizations, memberships, boards, feedback posts, votes, comments, and status history live in Postgres.
4. Membership roles are owner, admin, and member. Roles belong to memberships, not users.
5. Protected reads and mutations must derive identity from the server-side session and verify membership. Never trust a role or user ID supplied by the browser.
6. Enable RLS on every application table and expose no anon or authenticated table policies because browser clients do not query Supabase directly.
7. Use parameterized SQL for every dynamic value.
8. Store only session-token hashes. Never log plaintext session tokens or passwords.
Product requirements:
1. Users can register, sign in, restore a session, and sign out.
2. A signed-in user can create an organization and becomes its owner.
3. Owners and admins can create public feedback boards with unique slugs.
4. Public visitors can read a board and its roadmap state.
5. Authenticated users can submit feedback, vote once, remove their vote, and comment.
6. Owners and admins can move feedback among under_review, planned, in_progress, and shipped.
7. Every status change creates a history record.
8. Validate email, password, name, slug, title, description, and comment lengths.
9. Convert uniqueness violations into deterministic 409 responses.
10. Rate-limit registration and login before expensive bcryptjs work.
11. Include loading, empty, validation, unauthorized, rate-limited, and server-error states.
12. Build responsive and keyboard-operable screens with semantic landmarks, labels, visible focus, and a working skip link.
13. Add restrictive security headers, a 32 KiB JSON limit, same-origin mutation checks, HttpOnly cookies, SameSite=Lax cookies, and Secure cookies when APP_ORIGIN uses HTTPS.
Required screens:
- registration and sign in
- workspace onboarding
- board onboarding
- feedback dashboard
- public board
- feedback submission
- comments and roadmap filtering
- owner/admin status controls
Database requirements:
- versioned SQL migrations under supabase/migrations
- UUID primary keys
- timestamptz timestamps
- foreign keys with explicit delete behavior
- unique email and slug constraints
- composite membership and vote keys
- role and roadmap-status check constraints
- indexes for sessions, memberships, boards, posts, comments, and status history
Verification requirements:
- write failing tests before implementing domain behavior
- test duplicate registration races
- test expired and malformed sessions
- test mismatched Origin headers and oversized JSON
- test owner, member, outsider, and cross-tenant boundaries
- run typecheck, lint, unit/integration tests, production build, Playwright, axe, and production dependency audit
- start the production artifact and call /api/health
- deploy to Vercel and verify persistence after a fresh deployment
Explicit non-goals:
- Stripe billing
- email delivery and invitations
- password reset and email verification
- enterprise SSO
- duplicate merging
Do not add fake controls for deferred features.
Before writing code, return:
1. assumptions and security risks
2. route and component map
3. database schema and constraints
4. authorization matrix
5. migration order
6. implementation sequence
7. acceptance criteria
Stop after the plan so I can review it.
That last line forces an architecture review before the agent starts generating files.
Prompt 2: attack the tenant boundary
Run an adversarial architecture pass before implementation:
Audit the SignalRoom plan as if you are trying to cross tenant boundaries.
Check for:
- organization IDs trusted from browser input
- global roles instead of membership roles
- public payloads exposing email, password, session, or membership data
- duplicate votes
- missing foreign keys, constraints, or indexes
- plaintext session tokens
- registration and login resource-exhaustion paths
- cookies tied to NODE_ENV instead of the validated public origin
- uniqueness races that become 500 errors
- Supabase tables exposed through anon or authenticated roles
- privileged environment variables bundled by Vite
Return:
1. critical corrections
2. a revised authorization matrix
3. negative API tests for owner, member, outsider, and cross-tenant requests
4. database checks proving the public Supabase API is deny-by-default
Do not write application code yet.
Reject the plan if it cannot account for users substituting organization, board, or post IDs in requests.
Prompt 3: build identity before the dashboard
Implement users, sessions, organizations, memberships, authorization helpers, and onboarding first.
Requirements:
- bcryptjs password hashing with cost 12
- random opaque session tokens
- only SHA-256 session-token hashes stored in Postgres
- HttpOnly and SameSite=Lax cookies
- Secure cookies when APP_ORIGIN uses HTTPS
- registration and login rate limits before bcryptjs work
- deterministic 409 responses for concurrent duplicate registration
- owner/admin/member role checks
- safe onboarding for a user with no organization
- server-side DATABASE_URL only
- integration tests for cross-tenant writes and outsider moderation attempts
Run the focused tests and report the commands and actual results before adding feedback boards.
Build identity and memberships first; authorization mistakes otherwise spread into every later feature.
Prompt 4: build the feedback workflow
Implement public boards, feedback posts, votes, comments, roadmap filters, owner/admin status changes, and status history.
Requirements:
- unique board slugs
- validated post and comment lengths
- one vote per user and post enforced by a composite primary key
- vote toggling that returns the server's authoritative result
- public board responses that exclude private account and session fields
- owner/admin status controls enforced by the API
- a history record for every status change
- empty, validation, unauthorized, and controlled server-error states
- keyboard-operable native controls and visible focus
Add API and component tests. Report failures instead of removing assertions.
Prompt 5: migrate to Supabase and Vercel
SignalRoom began with local SQLite. That worked for one process but could not provide durable storage inside Vercel Functions. The migration prompt needs to change both persistence and runtime assumptions:
Migrate SignalRoom from synchronous local SQLite to Supabase Postgres and Vercel without changing the public API.
Requirements:
- replace synchronous database calls with an async database interface
- use pg with the Supabase transaction pooler
- translate SQLite schema syntax to PostgreSQL UUID, timestamptz, constraints, and indexes
- keep custom email/password authentication and opaque sessions
- keep every privileged database value server-only
- add versioned Supabase migrations
- enable RLS on all application tables and revoke anon/authenticated table access
- use PGlite for deterministic local Postgres-compatible tests
- keep a local Express entry point
- add a Vercel-compatible Express function entry point
- preserve same-origin /api calls from React
- derive the deployment origin safely from configuration
- pin the Node runtime
Add tests before changing behavior. Run the complete local gate, apply the remote migration, deploy, and verify that records survive another Vercel deployment.
Supabase provides durable storage, but Express still needs a Vercel-compatible serverless entry point.
Problems the browser exposed
Browser testing exposed these failures:
- The first server bundler rewrote a Node database import incorrectly. The build passed, but the compiled server could not start.
- A comment was saved, then the form tried to reset a stale React event target after an awaited request.
- Two onboarding forms reused old browser values between steps.
- Production mode forced Secure cookies during local HTTP testing because cookie behavior depended on
NODE_ENVinstead ofAPP_ORIGIN. - The skip link pointed to an ID that did not exist.
- A vote’s accessible label said
1 votes. - Vercel could serve the frontend, but local SQLite could not provide durable serverless storage.
- The first Supabase pooler connection failed certificate handling until the connection mode matched the provider configuration.
- The first database migration left the Supabase public-schema API surface too open, so a second migration enabled RLS and revoked browser roles.
These failures became regression tests and follow-up prompts. The broader pattern is covered in why vibe-coded apps break in production.
Test and deployment results
As verified on July 13, 2026, the pinned implementation returned:
npm test
Test Files 5 passed (5)
Tests 34 passed (34)
Exit code 0
npm run lint
Exit code 0
npm run build
Typecheck passed
Client 29 modules transformed
Server NodeNext TypeScript compile passed
Exit code 0
npm audit --omit=dev
Production vulnerabilities 0
Exit code 0
GitHub Actions also passed:
- install with
npm ci - unit and integration tests
- lint
- production build
- desktop and mobile Playwright tests
- axe checks
- production dependency audit
The hosted smoke test covered:
- HTTPS health response
- registration and secure session cookie issuance
- workspace and board creation
- feedback submission
- voting
- comments
- owner status changes
- public board access without a session
- retained records after a Git-triggered Vercel redeployment
The deployed app is signalroom-feedback-saas.vercel.app.
Deploying the same architecture
Follow Vercel’s Express deployment guide rather than treating a serverless function like a permanent process.
At minimum, configure:
DATABASE_URL=<Supabase pooled Postgres URL, server-only>
APP_ORIGIN=https://your-production-domain.example
SESSION_TTL_HOURS=168
TRUST_PROXY_HOPS=0
Store these through Vercel Environment Variables. Never prefix the database URL with VITE_; Vite exposes those variables to browser code.
Apply the Supabase migrations before the application receives traffic. Then deploy and run a real browser flow against the stable production alias, not only the generated deployment URL.
Finally, deploy once more and read the same board again. This confirms that records live in Postgres rather than Vercel’s ephemeral filesystem.
Security controls used here
SignalRoom uses practices covered by the OWASP Password Storage Cheat Sheet and Session Management Cheat Sheet. OWASP prefers Argon2id for new systems; this companion uses bcryptjs with cost 12 for compatibility and documents that tradeoff.
- password hashes never leave the server
- session cookies are HttpOnly and SameSite
- HTTPS sessions use Secure cookies
- plaintext session tokens are never stored in the database
- mutation requests reject mismatched origins
- request bodies have a fixed size limit
- public responses are shaped explicitly
- membership and tenant checks cover the protected routes exercised by the test suite
For checks outside this app’s scope, see the vibe coding security guide and AI-coded app launch audit.
What is still not production-ready
The app is deployed and tested, but it is not ready for important customer data:
- rate limits live in function memory rather than a shared store
- Supabase backup and restore have not been rehearsed
- automated browser tests cover Chromium, not Firefox or WebKit
- axe and keyboard checks passed, but a practical screen-reader review has not been completed
- there is no email verification, password reset, MFA, session-management screen, or account deletion flow
- no third-party penetration test has been performed
Those limits are acceptable for a demo. Fix the relevant ones before storing important data or charging users, using the vibe coding deployment checklist as the release gate.
Official resources
These are the external references used for the build:
- React documentation
- Vite guide
- Express documentation
- PostgreSQL constraints
- Supabase database documentation
- Supabase Row Level Security
- Supabase connection pooling
- Vercel Express deployments
- Vercel environment variables
- Vitest guide
- Playwright documentation
- axe-core accessibility engine
- OWASP Session Management Cheat Sheet
- OWASP Password Storage Cheat Sheet
FAQ
Can one prompt build a production-ready SaaS?
Not reliably. It stops being a prototype only after you test authorization, run browser QA, deploy it, and verify its behavior in production.
Why not use Supabase directly from React?
You can, but then RLS becomes the primary authorization boundary and every policy needs careful testing. SignalRoom keeps its existing Express API and custom session model, so the browser has no database credential and tenant checks stay in trusted server code.
Why did the app move away from SQLite?
SQLite worked locally and remains useful for single-process applications. Vercel Functions do not provide durable local application storage, so SignalRoom needed an external database. Supabase Postgres added hosted persistence, versioned migrations, and pooled serverless connections.
Does the first release need Stripe?
No. Billing is not part of the feedback loop. Add it when the product needs plans and entitlements, then make verified webhook state authoritative instead of trusting a browser redirect.
Is SignalRoom production-ready?
It is a publicly deployed, production-minded demo. Shared rate limiting, rehearsed database recovery, broader browser coverage, and practical assistive-technology testing still need work before calling it production-ready.
Copy the prompt, then inspect the result
Start with identity and memberships, then add the smallest useful feedback loop. Attack tenant isolation with negative tests, run the compiled application, deploy it, and confirm the data survives another deployment.
Copy Prompt 1, inspect the pinned SignalRoom implementation, or try the live demo. The full-stack prompt library has more starting points. Treat generated code as a draft until it passes tests and works after deployment.