Skip to main content

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

DevelopmentProduction
DatabaseropaApp-devropaApp
ClusterSame Atlas clusterSame Atlas cluster
Connection.env.localMONGODB_URIVercel 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

DevelopmentProduction
Instancedefinite-eagle-33.accounts.devProduction instance
Publishable keypk_test_...pk_live_...
Secret keysk_test_...sk_live_...
Session tokensShort-lived (60s), dev handshakeSelf-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 — syncClerkOrganization dynamically finds or creates the org in the dev Clerk instance

Domains

DevelopmentProduction
Tenant URL<shortName>.localhost:3000<shortName>.rat.gd
NEXT_PUBLIC_SITE_DOMAINrat.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

  1. Set MONGODB_URI in .env.local: the database name can be ropaApp — the app automatically uses ropaApp-dev when NODE_ENV=development.

  2. Use dev Clerk keys: .env.local should have pk_test_ / sk_test_ keys (already the case — never commit live keys).

  3. Seed the dev org — call recreate-demo while 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 demo org 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 as org:admin.

  4. 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-demo calls syncClerkOrganization, which needs a userId to 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

SymptomCauseFix
Unauthorized on all writes in devDev MongoDB org has a production clerkOrganizationIdRun recreate-demo while signed in (dev DB + dev Clerk keys)
Unauthorized on all writes in devrecreate-demo was called without a Clerk sessionCall /api/admin/sync-clerk-membership endpoint or recreate
Org not found / empty appDev server was started before .env.local was updatedRestart dev server after any .env.local change
localeDetection ignoredPassed as second arg to createIntlMiddleware (next-intl v4 ignores it)Set it inside defineRouting in routing.js