This file is the single end to end reference for understanding the repository and deploying it online.
It covers:
- What this codebase does
- What runs where
- The exact environment variables used by the backend, frontend, contracts, and wallet integration
- How to deploy PostgreSQL with Supabase
- How to deploy Redis with Upstash
- How to deploy the Next.js frontend on Vercel
- How to deploy the Fastify backend and worker process
- What to replace in each step
This guide is written for the current repository state and the current Vercel and Supabase documentation patterns as of June 2026.
VERIDAQ is a privacy preserving academic credential system.
The moving parts are:
packages/frontendis the Next.js 15 App Router frontendpackages/backendis the Fastify 5 API server, Prisma app, and BullMQ worker hostpackages/contractsis the Foundry smart contract projectpackages/circuitsis the Circom circuit and trusted setup artifacts
The runtime split matters:
- The frontend can be hosted on Vercel
- The database can be hosted on Supabase
- Redis can be hosted on Upstash
- The backend should run on a long lived Node host, not as a pure Vercel frontend deployment
Why that split exists in this repo:
- The backend starts a real Fastify server in
packages/backend/src/server.ts - The backend connects to PostgreSQL through Prisma
- The backend connects to Redis through
ioredis - The backend imports a worker from
packages/backend/src/workers/batch.processor.ts - The frontend only needs to call the backend API through
NEXT_PUBLIC_BACKEND_URL
The request flow is:
- Browser loads the Next.js frontend
- Frontend calls the backend API using
NEXT_PUBLIC_BACKEND_URL - Backend reads and writes PostgreSQL through Prisma
- Backend queues work and rate limiting data in Redis
- Backend talks to Base Sepolia, contracts, and the circuit artifacts when needed
The key production implication is simple:
- Vercel is the right host for the frontend
- Vercel is not the right host for this backend without a rewrite into serverless route handlers and separate worker infrastructure
If you want the cleanest production setup with the least code change, use this stack:
- Frontend: Vercel
- Backend: Render, Railway, Fly.io, or a similar Node host
- PostgreSQL: Supabase
- Redis: Upstash
These are the important files in this repo:
- package.json
- packages/backend/src/config/index.ts
- packages/backend/src/server.ts
- packages/backend/prisma/schema.prisma
- packages/backend/prisma/seed.ts
- packages/backend/src/plugins/redis.ts
- packages/frontend/lib/api.ts
- packages/frontend/lib/auth.tsx
- packages/frontend/lib/wallet-provider.tsx
- packages/frontend/next.config.ts
- docker-compose.yml
The backend validates environment variables in packages/backend/src/config/index.ts. If one is missing or malformed, the server exits at startup.
These are required for the backend to start:
NODE_ENVPORTFRONTEND_URLBACKEND_URLDATABASE_URLREDIS_URLJWT_SECRETREFRESH_SECRETALCHEMY_API_KEYALCHEMY_BASE_SEPOLIA_URLCIRCUIT_ZKEY_PATHCIRCUIT_WASM_PATHENCRYPTION_KEY
These are also used by the backend but are optional in code:
JWT_EXPIRES_INREFRESH_EXPIRES_INEXTENSION_JWT_EXPIRES_ININSTITUTION_REGISTRY_ADDRESSCREDENTIAL_REGISTRY_ADDRESSREVOCATION_REGISTRY_ADDRESSPAYMASTER_VAULT_ADDRESSSUBSCRIPTION_MANAGER_ADDRESSZK_VERIFIER_ADDRESSENTRY_POINT_ADDRESSBUNDLER_RPC_URLAA_SIMPLE_ACCOUNT_FACTORY_ADDRESSAA_SIMPLE_ACCOUNT_OWNER_PRIVATE_KEYAA_SIMPLE_ACCOUNT_SALTPLATFORM_ADMIN_PRIVATE_KEYPLATFORM_ADMIN_ADDRESSSMTP_HOSTSMTP_PORTSMTP_USERSMTP_PASSEMAIL_FROMBASESCAN_API_KEYEXTENSION_ORIGINSCROSSMINT_SERVER_API_KEYCROSSMINT_WEBHOOK_SECRET
The frontend reads these in browser visible code:
NEXT_PUBLIC_BACKEND_URLNEXT_PUBLIC_WALLET_CONNECT_PROJECT_IDNEXT_PUBLIC_BASE_SEPOLIA_CHAIN_ID
The wallet provider also supports the standard WalletConnect and RainbowKit ecosystem variables, but this repo only reads NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID directly.
From the source code:
packages/frontend/lib/api.tsreadsNEXT_PUBLIC_BACKEND_URLpackages/frontend/lib/wallet-provider.tsxreadsNEXT_PUBLIC_WALLET_CONNECT_PROJECT_IDpackages/backend/src/plugins/redis.tsreadsREDIS_URLpackages/backend/prisma/schema.prismareadsDATABASE_URLpackages/backend/src/config/index.tsreads the full backend env set
Use this naming pattern in production:
FRONTEND_URL=https://your-frontend-domainBACKEND_URL=https://your-backend-domainNEXT_PUBLIC_BACKEND_URL=https://your-backend-domain
If the frontend is on Vercel and the backend is on a separate host, these are different URLs on purpose.
If your frontend is deployed to Vercel at https://veridaq-official.vercel.app and your backend is deployed at https://api.veridaq.example, set:
FRONTEND_URL=https://veridaq-official.vercel.appBACKEND_URL=https://api.veridaq.exampleNEXT_PUBLIC_BACKEND_URL=https://api.veridaq.example
Then set EXTENSION_ORIGINS only if you actually use the browser extension.
For Supabase, keep one of these patterns:
- Use the direct connection string for a long lived backend host
- Use the pooler connection string if your host is IPv4 only or you want pooling
Recommended for this repo:
- Backend host on Render, Railway, or Fly.io: use the direct Supabase Postgres URL if your network can reach it
- If that host cannot use direct IPv6, use Supabase Supavisor pooler session mode or transaction mode depending on your runtime
Because this backend is a long lived Node server, the direct connection or session pooler is usually the better fit than transaction mode.
Set:
REDIS_URL=rediss://default:YOUR_UPSTASH_PASSWORD@YOUR-REDIS-HOST:6379
Do not hardcode the literal secret in documentation or commits.
Set:
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_walletconnect_project_id
Without it, the frontend falls back to YOUR_PROJECT_ID, which is only a placeholder.
Supabase is the correct PostgreSQL provider for this repo if you want managed Postgres.
Supabase provides several database connection styles.
For this repo:
- Use the direct connection string if your backend host supports it and you want the simplest setup
- Use the shared pooler if your backend host is IPv4 only
- Use transaction pooler only if you must use a serverless or edge style runtime
The Supabase guidance currently says:
- Direct connection is best for persistent backend services and migrations
- Pooler session mode is the fallback for persistent services on IPv4 only networks
- Pooler transaction mode is best for serverless or edge functions
This repo uses a plain Prisma datasource:
url = env("DATABASE_URL")
There is no separate DIRECT_URL in the schema right now.
That means you should store the actual Supabase Postgres connection string in DATABASE_URL.
If you later add Prisma migrations that require both direct and pooled URLs, you can extend the schema, but that is not required for this codebase today.
Use the exact format Supabase gives you from the dashboard, but rename it in your deployment settings like this:
DATABASE_URL=postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres
or, if you are using the shared pooler session mode:
DATABASE_URL=postgres://postgres.<project-ref>:<password>@aws-<region>.pooler.supabase.com:5432/postgres
or, if you are using the shared pooler transaction mode:
DATABASE_URL=postgres://postgres.<project-ref>:<password>@aws-<region>.pooler.supabase.com:6543/postgres
Use SSL where possible.
Upstash Redis is the correct hosted Redis option for this repo.
The backend uses ioredis in packages/backend/src/plugins/redis.ts and BullMQ in the worker layer, so the Redis URL must be reachable by your backend host.
Set:
REDIS_URL=rediss://default:<upstash_password>@<upstash_host>:6379
The rediss:// scheme is correct for TLS.
Vercel is the right host for the frontend.
Deploy only packages/frontend as the Next.js app.
The frontend depends on:
NEXT_PUBLIC_BACKEND_URLNEXT_PUBLIC_WALLET_CONNECT_PROJECT_IDNEXT_PUBLIC_BASE_SEPOLIA_CHAIN_ID
Do not expect the current backend to work as a direct Vercel deployment without structural changes.
Reason:
- It is a Fastify server with explicit startup, plugin registration, and a persistent
/healthlistener - It uses Prisma and Redis as live services
- It starts a batch worker import at server bootstrap
That is a normal Node backend shape, not a pure frontend host shape.
If you want the backend on Vercel later, you would need a separate serverless redesign.
Deploy in this order:
- Supabase PostgreSQL
- Upstash Redis
- Contracts to Base Sepolia, if you are using live chain interactions
- Backend host
- Vercel frontend
- Update frontend and backend env vars with the final public URLs
The order matters because the frontend needs the backend URL, and the backend may need the database and Redis URL before it starts.
- Log in to Supabase.
- Open your project dashboard.
- Confirm the database password.
- Open the Connect panel and copy the connection string.
- Decide whether you will use the direct URL or the pooler URL.
For this repo, the direct URL is simplest if your backend host can use it.
In your production secret store or host environment panel, set:
DATABASE_URL=<supabase_connection_string>
If you are using local development, you can still keep the Docker Postgres URL in your .env file.
- Open Upstash.
- Create a Redis database.
- Copy the TLS URL.
- Store it as
REDIS_URLin your production backend environment.
Use one of these hosts:
- Render
- Railway
- Fly.io
- Another persistent Node host
Recommended shape:
- One web service for the API server
- One worker process for BullMQ jobs if the host supports process separation
If your host only gives you one process, make sure the worker code is included in the same Node process only if that is already supported by the app entrypoint.
Set these in the backend host environment:
NODE_ENV=productionPORT=4000or whatever your host requiresFRONTEND_URL=https://your-vercel-domainBACKEND_URL=https://your-backend-domainDATABASE_URL=<supabase_connection_string>REDIS_URL=<upstash_rediss_url>JWT_SECRET=<long_random_secret>REFRESH_SECRET=<another_long_random_secret>ALCHEMY_API_KEY=<alchemy_key>ALCHEMY_BASE_SEPOLIA_URL=https://base-sepolia.g.alchemy.com/v2/<alchemy_key>CIRCUIT_ZKEY_PATH=/app/packages/circuits/build/credential_final.zkeyif the backend runs in a containerCIRCUIT_WASM_PATH=/app/packages/circuits/build/credential_js/credential.wasmif the backend runs in a containerENCRYPTION_KEY=<64_hex_char_key>
Add any of the optional contract or email keys you actually use in production.
Run the backend host build or start command that matches your platform.
The important behavior is that the process must:
- Install Node 22 and pnpm 9 or the host equivalent
- Run Prisma generation and migrations if needed
- Start
packages/backend/src/server.ts
If the host supports a Docker build, that is often the cleanest option for this repository because the backend also expects circuit artifact paths and worker code.
Once the backend has a public HTTPS URL, set:
NEXT_PUBLIC_BACKEND_URL=https://your-backend-domain
This is the main frontend to backend bridge. It is consumed in packages/frontend/lib/api.ts.
- Push the repository to GitHub if it is not already there.
- Import the repository into Vercel.
- Set the root directory to the repository root.
- Confirm that Vercel detects Next.js.
- Set the build command to
pnpm build:frontend. - Set the output directory to
packages/frontend/.next.
Do not copy the build output to the repository root. That breaks the relative
trace paths inside .next and causes Vercel to look for files under
/node_modules.
For this repo, the frontend build uses:
next build packages/frontend
In the Vercel project settings, add these values:
NEXT_PUBLIC_BACKEND_URL=https://your-backend-domainNEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=<walletconnect_project_id>NEXT_PUBLIC_BASE_SEPOLIA_CHAIN_ID=84532
If the frontend needs any other NEXT_PUBLIC_... value later, add it here as well.
- Trigger the first deployment.
- Wait for the build to complete.
- Open the Vercel URL.
- Verify that the frontend can reach the backend.
If login fails, the first thing to check is whether the backend CORS origin matches the final Vercel domain.
The backend only allows requests from the configured frontend origin.
That logic lives in packages/backend/src/server.ts.
You must set:
FRONTEND_URL=https://your-vercel-domain
If FRONTEND_URL does not exactly match the deployed frontend origin, the backend will reject the browser request.
The auth flow also uses cookies:
- JWT refresh tokens are stored in httpOnly cookies
- The frontend keeps the access token in memory only
That means the frontend and backend must both use HTTPS in production.
Here is the practical replace list.
Keep these values for local work:
DATABASE_URL=postgresql://veridaq:veridaq_dev@localhost:5432/veridaqREDIS_URL=redis://localhost:6379FRONTEND_URL=http://localhost:3000BACKEND_URL=http://localhost:4000NEXT_PUBLIC_BACKEND_URL=http://localhost:4000
Replace these with production values only when you are deploying:
DATABASE_URLREDIS_URLFRONTEND_URLBACKEND_URLNEXT_PUBLIC_BACKEND_URL
Set:
NEXT_PUBLIC_BACKEND_URL=https://your-backend-domainNEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=...NEXT_PUBLIC_BASE_SEPOLIA_CHAIN_ID=84532
Set:
FRONTEND_URL=https://your-vercel-domainBACKEND_URL=https://your-backend-domainDATABASE_URL=...REDIS_URL=...JWT_SECRET=...REFRESH_SECRET=...ENCRYPTION_KEY=...ALCHEMY_BASE_SEPOLIA_URL=...
The repo includes a seed script at packages/backend/prisma/seed.ts.
It creates:
- Admin user
- Demo institution
- Demo employer
- Default claim definitions
Run:
pnpm db:migratepnpm db:seed
For production, you usually do one of these:
- Run migrations during deploy
- Run a one time seed only if the database is empty and you want the default accounts
Do not reseed production repeatedly unless you intentionally want to upsert the default demo rows.
This repo expects contract addresses and circuit artifacts to be available once you move beyond local development.
Important production variables include:
INSTITUTION_REGISTRY_ADDRESSCREDENTIAL_REGISTRY_ADDRESSREVOCATION_REGISTRY_ADDRESSPAYMASTER_VAULT_ADDRESSSUBSCRIPTION_MANAGER_ADDRESSZK_VERIFIER_ADDRESSAA_SIMPLE_ACCOUNT_FACTORY_ADDRESSENTRY_POINT_ADDRESS
If you deploy new contracts, update the backend env with the new addresses.
For the circuit files, the backend expects paths to the compiled artifacts:
CIRCUIT_ZKEY_PATHCIRCUIT_WASM_PATH
If you run the backend in Docker or on a container host, use absolute in container paths.
If you run the backend on the host machine, local relative paths may work if the process starts from the repository root.
Current Vercel guidance relevant to this repo:
- Environment variables are set per project and per environment
- Production, Preview, and Development variables are separate
vercel env pullcan sync local env files from the project- Next.js on Vercel gets automatic SSR, streaming, image optimization, and deployment preview support
For this repo, the most important Vercel env rule is this:
- Set
NEXT_PUBLIC_BACKEND_URLfor Production and Preview
Without that, the frontend will default to http://localhost:4000 and break once deployed.
Current Supabase guidance relevant to this repo:
- Direct connection is best for persistent backend services and migrations
- Pooler session mode is the fallback for persistent services on IPv4 only networks
- Pooler transaction mode is best for serverless and edge functions
- SSL should be used wherever possible
For this repo, because the backend is persistent, do not default to transaction pooler unless your host requires it.
Current Upstash practice for this repo:
- Use the
rediss://URL in production - Keep the URL secret out of git history
- Use the same
REDIS_URLkey in local and production, but with different values
If you want everything on one platform with almost no backend hosting work, the realistic options are not Vercel alone.
You would need either:
- A full backend host plus Vercel for the frontend
- Or a bigger rewrite so the backend becomes serverless friendly and the worker moves elsewhere
For this codebase, the first option is the correct one.
Before launch, confirm these are true:
- Frontend is deployed on Vercel
- Backend is deployed on a persistent Node host
- Supabase connection string is in backend
DATABASE_URL - Upstash connection string is in backend
REDIS_URL FRONTEND_URLmatches the live frontend domainBACKEND_URLmatches the live backend domainNEXT_PUBLIC_BACKEND_URLmatches the live backend domainNEXT_PUBLIC_WALLET_CONNECT_PROJECT_IDis set in Vercel- Circuit artifact paths are correct in production
- Contract addresses are filled if chain features are live
- HTTPS is enabled on both frontend and backend
This is the clean production naming set to use:
Backend:
NODE_ENV=productionPORT=4000FRONTEND_URL=https://your-frontend-domainBACKEND_URL=https://your-backend-domainDATABASE_URL=<supabase_connection_string>REDIS_URL=<upstash_rediss_url>JWT_SECRET=<long_random_secret>REFRESH_SECRET=<long_random_secret>ALCHEMY_API_KEY=<alchemy_key>ALCHEMY_BASE_SEPOLIA_URL=https://base-sepolia.g.alchemy.com/v2/<alchemy_key>CIRCUIT_ZKEY_PATH=/app/packages/circuits/build/credential_final.zkeyCIRCUIT_WASM_PATH=/app/packages/circuits/build/credential_js/credential.wasmENCRYPTION_KEY=<64_hex_char_key>
Frontend on Vercel:
NEXT_PUBLIC_BACKEND_URL=https://your-backend-domainNEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=<walletconnect_project_id>NEXT_PUBLIC_BASE_SEPOLIA_CHAIN_ID=84532
The correct production deployment for this codebase is:
- Vercel for
packages/frontend - Supabase for PostgreSQL
- Upstash for Redis
- A real Node host for
packages/backend
That matches the current code, the current env names, and the current deployment behavior without forcing a rewrite.
If you want, I can also turn this into a shorter checklist version for day of deployment, or a separate .env.example aligned with the production values above.