WatchVault is a digital passport platform for luxury watches. It combines a Next.js web app, an Express API, a Neon Postgres project with separate production and preview branches, Clerk-backed web authentication, and optional blockchain anchoring for provenance events.
- Blockchain-secured watch provenance on Ethereum
- Clerk-backed web authentication with legacy email/password and OAuth API paths still present in the backend
- Watch collection management with public passport URLs and QR codes
- Event timeline for service, authentication, transfer, and note records
- Optional image upload support
- Web, native, and smart contract code in one repository
WatchVault is one codebase with separate runtime surfaces:
- Frontend web app: Next.js
- Backend API: Express + TypeScript
- Database: Neon Postgres via Prisma
- Authentication: shared Clerk application for the hosted web app
- Smart contracts: optional Hardhat/Solidity package
Current hosted domains:
- Frontend production:
https://mywatchvault.app - Frontend preview branch alias (
staging):https://watch-vault-git-staging-trietlus-projects.vercel.app - Frontend preview deployment example:
https://watch-vault-kb7c1y1z3-trietlus-projects.vercel.app - Backend production:
https://api.mywatchvault.app - Backend preview branch alias (
staging):https://watch-vault-api-git-staging-trietlus-projects.vercel.app - Backend preview deployment example:
https://watch-vault-sj6g5o8bc-trietlus-projects.vercel.app
Important deployment notes:
- The frontend and backend are deployed as separate Vercel projects.
- Vercel
ProductionandPrevieware separate deployment contexts even when they run the same code. - Preview and production share the same Clerk instance but no longer share the same Neon branch.
- Both frontend and backend projects are Git-connected and support branch-specific preview env overrides.
- The backend currently stores uploads on local disk in development and in Vercel
/tmpat runtime./tmpis ephemeral and is not durable object storage.
WatchVault uses a simple three-lane workflow:
- Short-lived feature branches for isolated work
stagingfor integrated preview testing on Vercelmainfor production
Expected flow:
- Create a short-lived branch from
stagingormainfor a focused change. - Merge that feature branch into
stagingwhen it is ready for shared preview testing. - Verify the
stagingfrontend and backend previews on Vercel. - Merge
stagingintomainwhen the change is ready for production.
What not to do:
- Do not develop directly on
main. - Do not use
stagingas a personal scratch branch. - Do not let
stagingdrift for long periods without either promoting it tomainor cleaning it up.
- Next.js App Router
- TypeScript
- Tailwind CSS
- Axios
- Clerk
- Zustand
- Node.js
- Express
- TypeScript
- Prisma
- Neon Postgres
- Multer
- JWT plus Clerk token verification
- Hardhat
- Solidity
- Ethers.js
- Node.js 18+ and npm
- Git
- Neon project and database
- Clerk application
- Vercel account if you want the hosted topology
git clone https://github.com/trietlu/WatchVault.git
cd WatchVaultcd backend
npm install
cp .env.example .envSet the backend environment variables in backend/.env, then run:
npm run prisma:generate
npm run prisma:push
npm run devThe backend runs on http://localhost:3001.
cd frontend
npm install
cp .env.local.example .env.local
npm run devThe frontend runs on http://localhost:3000.
cd contracts
npm install
npx hardhat compile
npx hardhat nodeIn another terminal:
npx hardhat run scripts/deploy.js --network localhostClerk is the primary authentication surface for the hosted web app.
Minimum web auth configuration:
- Set
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYinfrontend/.env.local - Set
CLERK_SECRET_KEYinbackend/.env - Keep frontend and backend pointed at the same Clerk application if you expect the same users and sessions to work across environments
The backend still contains legacy JWT and provider-specific auth endpoints for compatibility, but the current web deployment expects Clerk to be the main login path.
Start the services in separate terminals:
# Terminal 1 - Backend
cd backend
npm run dev
# Terminal 2 - Frontend
cd frontend
npm run dev
# Terminal 3 - Blockchain (optional)
cd contracts
npx hardhat nodeOpen http://localhost:3000.
The intended hosted topology is:
- Web UI on
mywatchvault.app - API on
api.mywatchvault.app - Branch previews on Vercel preview URLs
The frontend discovers the backend through NEXT_PUBLIC_API_BASE_URL. If that value points at a dead host, the UI can appear empty because the upstream API requests fail.
# Backend
cd backend
npm run build
npm start
# Frontend
cd frontend
npm run build
npm startThe hosted deployment is intentionally split into separate frontend and backend Vercel projects:
- The frontend project serves the Next.js app on the public web domain.
- The backend project serves the Express API on
api.mywatchvault.app. - Frontend and backend can be redeployed independently.
Repo-local and local-only Vercel/Codex wiring:
.codex/config.toml: repo-local Codex MCP configurationfrontend/.vercel/project.json: local link betweenfrontend/and the frontend Vercel projectbackend/.vercel/project.json: local link betweenbackend/and the backend Vercel project
*.vercel/project.json files are local linkage metadata. Commit them only if you intentionally want reproducible repo-local Vercel linkage and your ignore rules allow it.
DATABASE_URL="postgresql://USER:PASSWORD@POOLER-HOST/DBNAME?sslmode=require"
DIRECT_URL="postgresql://USER:PASSWORD@DIRECT-HOST/DBNAME?sslmode=require"
HOST=0.0.0.0
PORT=3001
JWT_SECRET=replace-with-a-long-random-secret
API_BASE_URL="http://localhost:3001"
APP_BASE_URL="http://localhost:3000"
CLERK_SECRET_KEY=""
UPLOADS_DIR="uploads"
BLOCKCHAIN_ENABLED="false"
CHAIN_ENV="preview"
CHAIN_RPC_URL="https://sepolia.base.org"
CHAIN_ID="84532"
CHAIN_PRIVATE_KEY=""
CHAIN_CONTRACT_ADDRESS=""
BLOB_STORE_ID=""
BLOB_READ_WRITE_TOKEN=""Notes:
DATABASE_URLis the pooled Neon connection used by the runtime.DIRECT_URLis the direct Neon connection used by Prisma CLI workflows.APP_BASE_URLshould match the frontend origin.API_BASE_URLshould match the public API origin.CLERK_SECRET_KEYis required when the backend needs to verify Clerk tokens.CHAIN_ENV=previewuses Base Sepolia (84532) for local and Vercel Preview deployments.CHAIN_ENV=productionuses Base Mainnet (8453) for production deployments.CHAIN_PRIVATE_KEYandCHAIN_CONTRACT_ADDRESSmust point at a wallet/contract on the selected chain beforeBLOCKCHAIN_ENABLED=true.BLOB_STORE_IDis required for Vercel OIDC Blob uploads/deletes in deployed environments.BLOB_READ_WRITE_TOKENcan be used instead for legacy/manual Blob auth.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
NEXT_PUBLIC_API_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_APP_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_GOOGLE_CLIENT_ID=""
NEXT_PUBLIC_FACEBOOK_APP_ID=""Notes:
NEXT_PUBLIC_API_BASE_URLis the single most important frontend runtime setting. It determines which API host the app calls.NEXT_PUBLIC_APP_BASE_URLis used for public passport URL generation and other app-origin-aware features.- The Google and Facebook IDs remain available for app compatibility, but Clerk is the primary hosted auth surface.
The hosted frontend and backend each have their own Vercel environment variables.
Current environment model:
- Local frontend:
NEXT_PUBLIC_APP_BASE_URL=http://localhost:3000NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
- Local backend:
APP_BASE_URL=http://localhost:3000API_BASE_URL=http://localhost:3001DATABASE_URL/DIRECT_URLcurrently point at the Neon preview branch for isolated local testing
- Production frontend:
NEXT_PUBLIC_APP_BASE_URL=https://mywatchvault.appNEXT_PUBLIC_API_BASE_URL=https://api.mywatchvault.appNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<shared Clerk publishable key>
- Production backend:
APP_BASE_URL=https://mywatchvault.appAPI_BASE_URL=https://api.mywatchvault.appDATABASE_URL/DIRECT_URL-> Neon production branchCLERK_SECRET_KEY=<shared Clerk secret>
- Preview frontend for branch
staging:NEXT_PUBLIC_APP_BASE_URL=https://watch-vault-git-staging-trietlus-projects.vercel.appNEXT_PUBLIC_API_BASE_URL=https://watch-vault-api-git-staging-trietlus-projects.vercel.appNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<shared Clerk publishable key>
- Preview backend for branch
staging:APP_BASE_URL=https://watch-vault-git-staging-trietlus-projects.vercel.appAPI_BASE_URL=https://watch-vault-api-git-staging-trietlus-projects.vercel.appDATABASE_URL/DIRECT_URL-> Neon preview branchCLERK_SECRET_KEY=<shared Clerk secret>
Notes:
- Clerk remains shared between preview and production, so the same signed-in Clerk user is resolved against different database branches depending on whether the request goes to production or preview.
- The frontend preview env overrides are branch-specific because the frontend Vercel project is Git-connected.
- The backend preview URL overrides for
stagingare branch-specific because the backend Vercel project is Git-connected. - If you create additional feature-branch previews later, decide whether they should share the
stagingpreview backend or receive their own backend preview overrides.
Preview env scope for the backend uses two layers:
- Project-wide
Previewenvs for values that should stay the same on every non-mainbranch:DATABASE_URLDIRECT_URLJWT_SECRETCLERK_SECRET_KEYBLOCKCHAIN_ENABLEDCHAIN_ENV=previewCHAIN_RPC_URL=https://sepolia.base.orgCHAIN_ID=84532CHAIN_PRIVATE_KEYCHAIN_CONTRACT_ADDRESSBLOB_READ_WRITE_TOKEN
- Branch-specific
Previewenvs for values that depend on the actual preview URL:APP_BASE_URLAPI_BASE_URL
Why APP_BASE_URL and API_BASE_URL are branch-specific:
- auth.middleware.ts uses
APP_BASE_URLas ClerkauthorizedParties. - watch.controller.ts uses
APP_BASE_URLto build QR and public passport URLs. - file.controller.ts relies on the API origin model behind
API_BASE_URLwhen serving public file URLs.
Practical rule:
- If all preview branches share one Neon preview branch, keep
DATABASE_URLandDIRECT_URLat genericPreview. - If each feature branch gets its own backend/frontend URL pair, set
APP_BASE_URLandAPI_BASE_URLper branch. - If you later create one Neon branch per feature branch, then
DATABASE_URLandDIRECT_URLshould also become branch-specific.
The current Neon project layout is:
- Production branch: existing default production branch
- Preview branch:
preview(br-quiet-recipe-akt7rocp)
Production backend envs point at the production branch. Preview backend envs point at the preview branch. The preview branch was created from production and then diverges.
This repo is intended to be operated through MCP-backed automation when possible instead of manually editing vendor consoles.
Preferred workflow:
- Use Codex plus Vercel MCP to inspect deployments, domains, logs, and environment variables, and to redeploy or verify services.
- Use Codex plus Neon MCP to inspect projects and branches, run SQL, compare schemas, and prepare safe migrations.
- Treat Clerk as shared external auth infrastructure and update its app-facing settings through repository changes and Vercel environment variables first. Use the Clerk dashboard only when the same capability is not exposed through the active toolchain.
- Prefer changing vendor state through MCP-backed automation over manual console edits so the operational steps can be replayed and documented.
This keeps infrastructure changes more reproducible, reviewable, and easier to document than ad hoc console edits.
WatchVault/
├── .codex/ # Repo-local Codex/MCP configuration
├── backend/ # Express API
│ ├── api/ # Vercel API entrypoint
│ ├── prisma/ # Database schema and migrations
│ ├── src/
│ │ ├── config/ # Runtime configuration
│ │ ├── controllers/ # Request handlers
│ │ ├── middleware/ # Auth, upload, etc.
│ │ ├── routes/ # API routes
│ │ └── lib/ # Shared backend helpers
│ ├── uploads/ # Local development uploads
│ └── vercel.json # Backend Vercel routing config
├── frontend/ # Next.js application
│ ├── .vercel/ # Local Vercel project link
│ ├── public/ # Static assets
│ └── src/
│ ├── app/ # App Router pages
│ ├── components/ # Shared UI components
│ ├── lib/ # API client and config
│ └── stores/ # Client state
├── native/ # React Native app
└── contracts/ # Ethereum smart contracts
POST /auth/registerPOST /auth/loginPOST /auth/googlePOST /auth/facebook
POST /watchesGET /watchesGET /watches/:idPOST /watches/:id/imagesDELETE /watches/:id/images/:fileIdPOST /watches/:id/events
GET /passports/:publicIdGET /health
The repo currently relies mostly on manual and integration-style verification.
Suggested checks:
- Sign in through the hosted or local web app.
- Confirm the frontend is calling the intended API host.
- Verify the backend resolves the same user in Neon.
- Create a watch and confirm it appears in the collection.
- Open the public passport URL and verify the event timeline.
- Confirm
DATABASE_URLpoints at a reachable Postgres/Neon database. - Generate the Prisma client with
npm run prisma:generate. - Verify
backend/.envexists and contains a validJWT_SECRET.
- Confirm
frontend/.env.localexists. - Verify
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYis set for Clerk-backed login. - Clear
.nextand retry if the build cache is stale.
- Verify
NEXT_PUBLIC_API_BASE_URLpoints at the live backend. - Confirm the backend is healthy at
/health. - Confirm the signed-in account maps to a user row that actually owns watches in Neon.
- Verify
backend/uploads/watchesexists in local development. - Confirm the file is under the 8 MB limit and uses an allowed image format.
- On Vercel, remember uploads currently land in
/tmpand are not durable.
- Check the Vercel environment values for frontend and backend separately.
- Remember that preview and production are separate Vercel environments even if they currently share one Clerk and Neon environment.
- Create a feature branch.
- Make the code and doc changes together when you change infrastructure or environment behavior.
- Verify the local and hosted configuration still match the documentation.
- Open a pull request.