Dev vs Production Environments
Overview
The app runs against two completely separate stacks: a development environment (local machine) and a production environment (deployed). They share the same codebase but use different MongoDB databases, different Clerk instances, and different domains.
MongoDB
| Development | Production | |
|---|---|---|
| Database | ropaApp-dev | ropaApp |
| Cluster | Same Atlas cluster | Same Atlas cluster |
| Connection | .env.local → MONGODB_URI | Vercel env var |
The code automatically appends -dev to the database name when NODE_ENV=development, so MONGODB_URI can point to ropaApp in both environments — src/services/mongodb.js rewrites it to ropaApp-dev at runtime in dev. Atlas creates ropaApp-dev automatically on first write.
Clerk
| Development | Production | |
|---|---|---|
| Instance | definite-eagle-33.accounts.dev | Production instance |
| Publishable key | pk_test_... | pk_live_... |
| Secret key | sk_test_... | sk_live_... |
| Session tokens | Short-lived (60s), dev handshake | Self-validating JWTs |
Clerk organizations are instance-scoped: an org ID from production does not exist in the dev Clerk instance. This is why dev and prod must have separate MongoDB databases — the clerkOrganizationId stored on each org document only works against the matching Clerk instance.
KNOWN_CLERK_ORG_IDS (src/constants/constants.js)
This map holds hardcoded Clerk org IDs for orgs that may be recreated without an authenticated user (e.g. via /api/admin/recreate-demo).
- Production (
pk_live_): uses the hardcoded prod org ID - Development (
pk_test_): map is empty —syncClerkOrganizationdynamically finds or creates the org in the dev Clerk instance
Domains
| Development | Production | |
|---|---|---|
| Tenant URL | <shortName>.localhost:3000 | <shortName>.rat.gd |
NEXT_PUBLIC_SITE_DOMAIN | rat.gd (unused locally) | rat.gd |
getHostnameServer extracts the shortName from the hostname:
demo.localhost:3000→"demo"demo.rat.gd→"demo"localhost:3000(no subdomain) →"demo"(fallback)
Clerk Middleware (src/proxy.js)
The middleware matcher excludes /api/* routes by default (Next.js convention). Admin API routes under /api/admin/* are explicitly included so that auth() works inside them — they need Clerk auth context even though they're not user-facing protected routes.
matcher: [
"/((?!api|_next|_vercel|.*\\..*).*)",
"/api/admin/(.*)", // needs auth() context
"/([\\w-]+)?/users/(.*)",
]
Admin routes are not run through the intl middleware — the middleware short-circuits with NextResponse.next() for /api/admin/* paths.
Setting Up a Dev Environment from Scratch
-
Set
MONGODB_URIin.env.local: the database name can beropaApp— the app automatically usesropaApp-devwhenNODE_ENV=development. -
Use dev Clerk keys:
.env.localshould havepk_test_/sk_test_keys (already the case — never commit live keys). -
Seed the dev org — call
recreate-demowhile signed in to the dev app:// In browser console at http://demo.localhost:3000/en/e (while logged in)fetch("/api/admin/recreate-demo", {headers: { "x-admin-secret": "<ADMIN_SECRET>" }}).then(r => r.json()).then(console.log)This creates the
demoorg in MongoDB and links it to a Clerk org in the dev instance. Because the request goes through the Clerk middleware,auth()resolves your user ID and you are automatically added asorg:admin. -
Verify: navigate to the app and confirm you can edit org data without an "Unauthorized" error.
Why must you be signed in for step 3?
recreate-democallssyncClerkOrganization, which needs auserIdto set the org creator/admin. If called unauthenticated, the Clerk org is created with no members, and all subsequent write operations return "Unauthorized".
Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Unauthorized on all writes in dev | Dev MongoDB org has a production clerkOrganizationId | Run recreate-demo while signed in (dev DB + dev Clerk keys) |
Unauthorized on all writes in dev | recreate-demo was called without a Clerk session | Call /api/admin/sync-clerk-membership endpoint or recreate |
| Org not found / empty app | Dev server was started before .env.local was updated | Restart dev server after any .env.local change |
localeDetection ignored | Passed as second arg to createIntlMiddleware (next-intl v4 ignores it) | Set it inside defineRouting in routing.js |