Sense 48: The Stage — dev/prod plane separation
Sense 48 · In bloom · Last touched 2026-08-23
- last_verified: 2026-07-27
Synced from
docs/design/sense_48_the_stage.mdin the engine repo — that’s the source; this page is a build-time mirror.
Working principle: prod is the system. Dev is a controlled affordance for developers.
Why this Sense exists
Section titled “Why this Sense exists”Until 2026-06, jinflow ran a single Fly app (this-is-jinflow), a single R2 bucket (jinflow-demo), and a single Clerk app (the dev instance intense-lionfish-66). Customer-facing surfaces (rmc.jinflow.io, vai.jinflow.io, etc.) all routed through that one stack. There was no real distinction between “what’s in production” and “what’s being tested” — every change went live on customer surfaces the moment it shipped.
On 2026-06-13, that monolith split:
- A second Fly app (
jinflow-dev) was created in thejazzisnoworg, servingdev.jinflow.io. - A second R2 bucket (
jinflow-dev, EU jurisdiction) was created to hold dev KLS data. - A second Clerk app (jinflow’s production Clerk instance, custom domain
clerk.jinflow.io) was created and bound to customer-facing surfaces; the dev instance (intense-lionfish-66) is now reserved for development work.
This split needs a coordinated spec — the env distinction has to flow through every layer (auth, app code, data, membership) and every CLI command. Without that coordination, the split risks becoming silent surface area where developers can accidentally touch production. The Stage is that spec.
Vocabulary
Section titled “Vocabulary”| Term | Meaning in this Sense |
|---|---|
| env | A namespace value, either dev or prod. Carries through auth, app, data, and membership planes. Declared per-tenant in tenant.yml. |
| developer | A jazzisnow staff member with platform_role: jazzisnow_dev on their prod Clerk publicMetadata. Distinct from any per-tenant role they may hold. |
| end user | Any user who is not a developer — customers, analysts, support consumers. The vast majority of users. |
| elevated | Operating with a platform_role. The CLI and JinDesk surface this explicitly when active. |
| prod-implicit | The CLI’s default mode for end users: every command targets prod without flags. |
| opt-in dev | The CLI’s default mode for developers: prod is still implicit per-tenant, but dev capabilities become reachable through --env flags and explicit configuration. |
The core asymmetry
Section titled “The core asymmetry”The dev/prod split is not symmetric. End users live entirely in prod. They never see, configure, or learn about “dev.” Only developers operate in dev.
This asymmetry shapes every design decision below:
- The CLI’s default behaviour is prod-implicit. End users never need an
--envflag. - Dev capabilities are opt-in through layered signals (local config + cloud authorization).
- The dev/prod indicator on JinDesk pages is invisible for prod (the default), prominent for dev.
- Confirmation prompts gate prod operations from developers, but never appear for end users (prod IS the system for them, so there’s nothing to disambiguate).
Contrast with the rejected “kubectl-context” model where users explicitly switch between dev and prod modes. That model adds friction for everyone, primarily to protect against developer mistakes that don’t apply to end users. The Stage rejects that — env is a property of the tenant, not the session.
The four planes
Section titled “The four planes”| Plane | What env determines | Concrete instances today |
|---|---|---|
| Auth | Which Clerk app signs tokens, manages users, hosts sign-in | prod: clerk.jinflow.io ; dev: intense-lionfish-66.accounts.dev |
| App | Which Fly app’s JinDesk image is running | prod: this-is-jinflow (becomes jinflow-prod) ; dev: jinflow-dev |
| Data | Which R2 bucket holds the KLS files | prod: jinflow-prod (TBD, replacing jinflow-demo, EU jurisdiction) ; dev: jinflow-dev (already EU) |
| Membership | Which principal_registry the cloud JinDesk checks, materialized from which Clerk’s publicMetadata | per env, independent — a user invited to prod-rmc is NOT automatically a member of dev-rmc |
The next four subsections walk through each plane, showing how env routes through the code path, what gets configured where, and what fails if it’s missing.
Auth plane
Section titled “Auth plane”Each Clerk app has its own:
- Frontend API host —
clerk.jinflow.io(prod) vsclerk.intense-lionfish-66.accounts.dev(dev). - Account Portal —
accounts.jinflow.io(prod) vsaccounts.intense-lionfish-66.accounts.dev(dev). - Publishable key —
pk_live_…(prod) vspk_test_…(dev), both decoding to their respective Frontend API hosts. - Secret key —
sk_live_…(prod) vssk_test_…(dev). - User table — independent. A user signing up on dev does not exist on prod.
- JWT templates —
cliandjinflow-launcherare configured identically on both (same name, sameaud, same 30-day lifetime), so token verification code stays uniform.
CLI key resolution per command target (the dual-keys machinery in secrets.yml):
clerk: prod: publishable_key: pk_live_<decodes to clerk.jinflow.io> secret_key: sk_live_… dev: publishable_key: pk_test_<decodes to clerk.intense-lionfish-66.accounts.dev> secret_key: sk_test_…Plus the validation already shipped in launch.py (_is_jinflow_clerk_host): the prod Frontend API matches the .jinflow.io suffix allow-list; the dev Frontend API is named explicitly in _JINFLOW_KNOWN_CLERK_HOSTS. Foreign keys (e.g. weave) are refused.
Failure modes:
- Dev keys absent, command targets dev tenant → “dev environment is not configured for this CLI.”
- Dev keys present but decode to a non-jinflow host →
launch.pyvalidation strips them, command falls through to “no dev keys” error. - Prod keys absent → CLI cannot talk to any prod tenant. Refuses with hint to run
jin login(which writes tosecrets.yml).
App plane
Section titled “App plane”Each Fly app holds a deployed JinDesk image. Today:
| Fly app | Org | Org-canonical name (post-rename) | Hostnames |
|---|---|---|---|
this-is-jinflow | jazzisnow | jinflow-prod (Op 3 task #22) | rmc.jinflow.io, vai.jinflow.io, numetrix.jinflow.io, hrcentral.jinflow.io, millesime.jinflow.io, this-is.jinflow.io, app.jinflow.io |
jinflow-dev | jazzisnow | (already canonical) | dev.jinflow.io, jinflow-dev.fly.dev |
jinflow-proxy | personal → jazzisnow (Op 2 task #21) | (unchanged) | proxy.jinflow.io |
JinDesk codebase is identical on both. What differs is which image tag is deployed and which Fly secrets (env vars) are set on the machine:
JINFLOW_ENV=prodon jinflow-prod,JINFLOW_ENV=devon jinflow-dev.- Clerk keys differ (prod vs dev).
R2_BUCKETdiffers (jinflow-prodvsjinflow-dev).JINFLOW_ADMIN_TOKENdiffers (independent rotations).
just prod-promote ships the dev image to the prod Fly app, leaving secrets intact on both sides. The image is the only thing that crosses the env boundary.
Note on the verb’s home: promotion is an infrastructure operation (deploys the Fly app), not an analytical verb. It belongs on
just, alongsidejust shipandjust dev-deploy, not onjin(which is reserved for tenant data operations —jin make,jin ship,jin explore). The original spec proposedjin promote; that drifted intojust prod-promoteonce we sharpened the line (2026-06-23). The underlying logic still lives injinflow/cli/commands/promote.pyand remains testable as a module; thejustrecipe just invokes it viapython3 -m jinflow.cli.commands.promote.
Failure modes:
- Image runs on the wrong Fly app (mismatched secrets) → /sign-in either fails or routes to the wrong Clerk. Detected by smoke tests.
- Wrong
JINFLOW_ENVenv var → about page shows wrong badge; CLI confirmation prompts misfire. Not a security issue but confusing.
Data plane
Section titled “Data plane”Each R2 bucket holds tenant KLS files. The bucket is determined by tenant’s declared env, not by which JinDesk reads from it:
jinflow-prod(post #25 R2 reorg) holds KLS files forenvs: [prod]tenants (rmc, vai, numetrix, hrcentral, millesime).jinflow-devholds KLS files forenvs: [dev]tenants. Currently empty.- A multi-env tenant (
envs: [dev, prod]) has two independent KLS copies, one per bucket.
jin ship’s job is to write to the right bucket. The bucket is derived from the tenant’s envs: declaration plus the optional --env flag (used when ambiguous or overriding).
Customer data flow is direct: a numetrix.rmc make → ship goes straight to jinflow-prod bucket. There is no dev-staging step.
Failure modes:
- Bucket mismatch (e.g.
R2_BUCKET=jinflow-devonjinflow-prodFly app) → JinDesk fails to load any KLS, returns 503. Detected by smoke test on a known tenant. - Bucket access denied (R2 credentials wrong) → same, with auth error in logs.
- Ship to wrong bucket → cloud JinDesk continues to serve old KLS; new KLS lands in dev (or vice versa) and is silently unused. Caught by the developer-only target-env one-liner in CLI output.
Membership plane
Section titled “Membership plane”Each env has its own principal_registry, materialized per-tenant from that env’s Clerk publicMetadata:
publicMetadata (prod Clerk for Mig): jinflow: platform_role: jazzisnow_dev tenants: numetrix.rmc: identity: owner principal_id: mig
publicMetadata (dev Clerk for Mig, separate user): jinflow: platform_role: jazzisnow_dev tenants: numetrix.test_fixture: identity: owner principal_id: migA user has the same principal_id across envs (a convention, not enforced) but different Clerk user IDs — they’re different Clerk accounts entirely. publicMetadata structure is identical; content differs.
jin sync-principals <tenant> resolves env from the tenant, queries that env’s Clerk, materializes into that env’s AFS principal YAMLs, and bakes into that env’s KLS. Cross-env reconciliation is a separate, explicit operation (currently out of scope for this Sense).
Failure modes:
- User is owner on dev-rmc, expects to be owner on prod-rmc → not granted. Cleanly intentional: env isolation.
- AFS principal YAML has stale clerk_user_id (old Clerk app’s user) →
jin sync-principalsupdates from current Clerk; if user removed themselves from publicMetadata, principal is archived.
What promotes between dev and prod
Section titled “What promotes between dev and prod”Only app artifacts promote. KLS data never does.
jin shipwrites a tenant’s KLS to whichever R2 bucket matches the tenant’s declared env. Customer data goes directly to prod; it does not flow between buckets.just prod-promote(new verb defined here) takes the JinDesk image currently running onjinflow-dev, retags it, and deploys it tojinflow-prod. Bit-identical — no rebuild between dev validation and prod deployment.
Promote requires a smoke-test gate
Section titled “Promote requires a smoke-test gate”just prod-promote refuses to run unless the image currently on jinflow-dev has passed the agreed-upon smoke-test set since the image was deployed. The smoke-test attestation is recorded in state/smoke_log.jsonl (engine repo) and verified at promote time.
Bypassing requires an explicit --force-no-smoke flag and is logged separately as a policy exception.
The minimum smoke set (defined here, lives in tests/smoke/):
- Sign-in flow — open
dev.jinflow.io/sign-in, complete a headless Clerk OAuth round-trip against the dev Clerk app, end up at/adminwith a valid__sessioncookie. - KLS load — request
/numetrix.rmc/dimensions/cases(or equivalent) against a known dev fixture tenant, expect a 200 with the expected row count tolerance. - Health endpoint —
GET /healthreturns 200. - Schema-version round-trip — request the schema endpoint, verify it matches the image’s expected schema SHA.
- API capability gate — without auth, a privileged endpoint (e.g.
/api/invitations) returns 401; with valid Bearer token, it returns either 403 or 200 (not 500).
A passing smoke run writes an attestation record:
{"smoke_run_id": "smoke_…", "image_sha": "…", "ran_at": "2026-06-14T13:42:01Z", "ran_by": "ci-runner-3@github", "results": [{"test": "sign-in", "ok": true, "ms": 1230}, …], "all_passed": true}just prod-promote reads the most recent attestation from state/smoke_log.jsonl, verifies image_sha matches what’s currently deployed on jinflow-dev, and proceeds only if all_passed: true.
Promote leaves a dual audit trail
Section titled “Promote leaves a dual audit trail”Every promote writes to two logs:
state/promote_log.jsonlin the engine repo — developer-facing audit. Records who, when, image SHA, source dev image SHA, smoke-test attestation reference, target Fly app, target image tag.<prod-AFS>/state/promote_log.jsonlin the prod tenant root — operations-facing audit. Same record, written to production-side AFS, visible to operations roles via the about page.
Two audiences, two log locations, one event.
{"promote_id": "promote_…", "promoted_at": "2026-06-14T13:42:01Z", "promoted_by": "mig", "image_sha": "abc123…", "source_dev_image": "abc123…", "smoke_run_id": "smoke_…", "smoke_attested_at": "2026-06-14T13:41:50Z", "target_fly_app": "jinflow-prod", "target_image_tag": "v0.55.2-prod"}How tenants declare env
Section titled “How tenants declare env”Per-tenant declaration in tenant.yml, always a list:
envs: [prod] # single-env tenant (rmc, vai, customer tenants)# ORenvs: [dev, prod] # multi-env tenant (development copies, staging fixtures)# ORenvs: [dev] # dev-only test fixtureDefault if unspecified: envs: [prod] (safe customer-tenant assumption — refuses to “downgrade” to dev silently).
Uniform list shape (no bare-string env: form) simplifies parsing and avoids two-form ambiguity.
Multi-env tenants require explicit --env when commands are ambiguous.
Migration of existing tenants
Section titled “Migration of existing tenants”All current tenants today implicitly live on prod (the only env that exists). Migration is conservative:
- Step 1: Add
envs: [prod]to every existing tenant.yml. No behaviour change. - Step 2: Land Sense 48 CLI changes — env-aware ship/invite/sync-principals/revoke.
- Step 3: Optionally declare
envs: [dev, prod]for tenants where a dev copy makes sense (e.g.numetrix.inspireas a dev playground). - Step 4: Create dev-only fixtures (
envs: [dev]) for new developer-test tenants.
No existing tenant is silently moved to dev. The migration is explicit and reversible.
How the CLI picks env
Section titled “How the CLI picks env”Decision logic for every env-routing command:
--envflag passed → use it (after authorization check). If user lacks dev access and--env devis passed → refuse with the developer-mode-not-configured error.- No flag, read tenant’s declared env:
- Single-env (e.g.
envs: [prod]) → use it silently. - Multi-env (e.g.
envs: [dev, prod]) → error: “ambiguous — tenant lives in [dev, prod], pass--env devor--env prod”.
- Single-env (e.g.
- Non-tenant-scoped commands (e.g.
jin login) → use the command-specific default. Forjin login, that’s prod.
Local launcher jin explore is the one exception: it defaults to --env dev for safety, regardless of tenant. Explicit --env prod opts into prod Clerk for the local launch. Rationale: casual local testing must not pollute prod Clerk’s user state.
Command reference
Section titled “Command reference”| Command | Default env source | --env flag effect | End-user visibility of flag |
|---|---|---|---|
jin invite <email> <tenant> | tenant’s envs: (single-env) or error (multi-env) | Override; require dev access for --env dev | Hidden if no dev keys present |
jin revoke <user> <tenant> | same | same | Hidden if no dev keys present |
jin sync-principals <tenant> | same | same | Hidden if no dev keys present |
jin ship <tenant> | same | same | Hidden if no dev keys present |
jin make <tenant> | (no env interaction — runs locally against AFS) | n/a | n/a |
jin login | prod | Adds a dev session (does not replace prod); --env both runs both | Hidden if no dev keys present |
jin logout | all sessions | Clears only the specified session | Hidden if no dev keys present |
jin explore <tenant> | always dev (safety) | --env prod opts into prod Clerk for the local launch | Visible if dev keys present; hidden otherwise |
just prod-promote | always (dev image → prod Fly app); not env-routing in the per-tenant sense | n/a | Hidden if not jazzisnow_dev |
jin smoke | always dev (runs against jinflow-dev) | n/a | Hidden if not jazzisnow_dev |
How “developer mode” is detected (the layered model)
Section titled “How “developer mode” is detected (the layered model)”A non-developer running jin ship --env dev rmc should get a clean refusal, not a partial failure. Three layers gate dev access; all three are required for actual dev operations:
| Layer | Signal | What it gates | Where it lives |
|---|---|---|---|
| L1 — local secrets | Dev Clerk keys present in secrets.yml under clerk.dev.* | CLI knows dev exists at all; --env flag visible in help | ~/.jinflow/secrets.yml |
| L2 — local config | Inferred from L1, or explicit developer: true in config | CLI surfaces dev affordances in interactive prompts | ~/.jinflow/config.yml (optional) |
| L3 — cloud authorization | platform_role: jazzisnow_dev in user’s prod Clerk publicMetadata | Server-side enforcement — dev JinDesk + dev Clerk reject requests from users without this role | Clerk dashboard (sets via API or UI) |
Defence in depth: a leaked dev key alone gets you nothing without the cloud-side role. Conversely, having the cloud role but no local keys means the CLI doesn’t know dev exists from your perspective (a clean “configure dev keys to use dev commands” hint).
Exception —
jin shipkeys ondeploy_token, not Clerk keys. Ship is the one Sense 48 command that doesn’t authenticate a user via Clerk at all; it writes KLS to R2 using thedeploy_token(Sense 25 Phase 3a). Conflating its env gate with “dev Clerk keys” would force operators to configure Clerk keys they never use, just to ship data. Since 2026-06-26,jin ship’s “is this an operator?” signal is presence ofdeploy_token(env varJINFLOW_DEPLOY_TOKENor~/.jinflow/secrets.yml). The layered model above still applies tojin invite,jin login, and other user-auth commands — they read [[has_dev_clerk_keys]]. Ship reads [[has_deploy_token]]. See also [[feedback_infra_gates_not_user_gates]].
Graceful refusal message for users without dev configuration:
ERROR: dev environment is not configured for this CLI.This command is reserved for jazzisnow developers.
If you should have dev access: • Confirm with an admin that platform_role=jazzisnow_dev is set on your prod Clerk user. • Add dev Clerk keys to ~/.jinflow/secrets.yml under clerk.dev.*. • Run `jin login --env dev` to mint a dev session.The UI surface
Section titled “The UI surface”JinDesk shows a small badge in the page header indicating environment:
prod→ no badge (the default, invisible to end users).dev→ yellowDEVchip with the tenant slug, always visible.local-launcher→ a separate indicator showingLOCAL, since that’s about where JinDesk runs, not which env it’s pointed at. May co-exist with dev/prod env badge.
For elevated developers (platform_role: jazzisnow_dev), an additional badge appears next to the username: DEV ELEVATED. This reminds the developer that they operate with cross-tenant privileges that an end user does not have. The badge is shown on every page where the user’s identity is rendered (header, account dropdown, account portal links).
The about page expands this with full context: tenant slug, env, Fly app name, Clerk instance, R2 bucket, image SHA, schema version, smoke attestation timestamp (when applicable). For debugging.
Mockup — header on dev
Section titled “Mockup — header on dev” ┌─────────────────────────────────────────────────────────────────┐ │ jinflow numetrix.rmc ⚙ Mig▼ [DEV ELEV] │ │ [DEV] │ └─────────────────────────────────────────────────────────────────┘Mockup — header on prod (for end user)
Section titled “Mockup — header on prod (for end user)” ┌─────────────────────────────────────────────────────────────────┐ │ jinflow numetrix.rmc ⚙ Ronnie ▼│ └─────────────────────────────────────────────────────────────────┘Mockup — header on prod (for developer)
Section titled “Mockup — header on prod (for developer)” ┌─────────────────────────────────────────────────────────────────┐ │ jinflow numetrix.rmc ⚙ Mig▼ [DEV ELEV] │ └─────────────────────────────────────────────────────────────────┘(no env badge on prod — the absence IS the signal. DEV ELEVATED stays on every page for the developer.)
CLI surface — developer feedback + prod confirmation
Section titled “CLI surface — developer feedback + prod confirmation”For elevated developers, every env-routing command prints a one-liner showing the resolved target before the operation runs:
→ targeting numetrix.rmc on PROD (clerk.jinflow.io, jinflow-prod R2)Sent invitation to ronnie@lita29.ch as owner.End users do not see this — for them, prod is the system, so saying “targeting prod” is noise.
Prod operations from a developer require a confirmation step unless --yes is passed:
→ targeting numetrix.rmc on PROD This will refresh the customer-facing JinDesk.Confirm? [y/N]End users don’t see the confirmation either — they only have prod, so there’s nothing to disambiguate. The friction exists specifically to catch developer “I meant dev” mistakes.
The set of operations that trigger prod confirmation:
jin shipto prodjin inviteto prodjin revokefrom prodjin sync-principalsagainst prod (writes can affect membership)just prod-promote(always — it always targets prod by definition)
The set of operations that don’t require confirmation even for developers:
jin make— local-only, no remote side effectjin login— interactive flow has its own confirmation built injin explore— local launcher, dev-by-default, no remote side effect
How jin login works in the asymmetric model
Section titled “How jin login works in the asymmetric model”A developer can hold simultaneous Clerk sessions — one against prod, one against dev. The CLI caches them as separate token files (e.g. ~/.jinflow/sessions/prod.token and ~/.jinflow/sessions/dev.token), and picks the right one per command based on the tenant’s resolved env.
Behaviour:
jin login— signs in to prod (default; matches end-user convention). If a prod session already exists, refreshes it.jin login --env dev— signs in to dev in addition, not replacing prod.jin login --env both— runs both sign-in flows sequentially.jin logout— clears all cached sessions.jin logout --env dev— clears only dev session.
Session cache format:
~/.jinflow/sessions/ prod.token # ~30-day JWT from prod Clerk's `cli` template dev.token # ~30-day JWT from dev Clerk's `cli` template prod.meta.json # {clerk_user_id, expires_at, frontend_api} dev.meta.json # same shapePer-command session selection:
- Resolve env from tenant (or flag).
- Read
<env>.token. - If expired or missing → prompt to run
jin login --env <env>. - Send as
Authorization: Bearer …to JinDesk endpoint matching the env.
Non-developers (no dev keys in secrets.yml) see no --env flag in help — jin login is unconditionally prod for them.
Concrete sequences
Section titled “Concrete sequences”Sequence 1: End user signs in to rmc.jinflow.io
Section titled “Sequence 1: End user signs in to rmc.jinflow.io”1. User opens https://rmc.jinflow.io/sign-in2. JinDesk's ClerkProvider mounts using PUBLIC_CLERK_PUBLISHABLE_KEY (Fly secret on this-is-jinflow → pk_live_… → decodes to clerk.jinflow.io)3. Clerk SDK opens hosted sign-in at clerk.jinflow.io4. User authenticates (Google OAuth)5. Clerk returns __session cookie scoped to .jinflow.io6. User lands at /numetrix.rmc dashboard7. Capability check: principal_registry in numetrix.rmc's prod KLS contains user's prod Clerk user_id with identity=owner/analyst/etc.8. User sees dataNo dev anywhere in this sequence. The user has no idea dev exists.
Sequence 2: Developer ships a code change
Section titled “Sequence 2: Developer ships a code change”1. Developer commits code change to engine repo.2. `just ship --now` builds JinDesk image + CLI binary.3. Image is pushed to `jinflow-dev` Fly app (default ship target).4. Developer opens dev.jinflow.io, signs in via dev Clerk, exercises the new code against a dev tenant (or runs `jin smoke` for automated coverage).5. Smoke run writes attestation to state/smoke_log.jsonl.6. Developer runs `just prod-promote`.7. CLI: "→ promoting image abc123 from jinflow-dev to jinflow-prod. Smoke attestation found (smoke_…, all 5 tests passed)." "Confirm prod promotion? [y/N]"8. Developer confirms.9. CLI calls `fly image … -a jinflow-prod` to deploy the same image.10. Fly secrets on jinflow-prod are unchanged (Clerk keys remain prod, R2 bucket remains jinflow-prod, etc.).11. Promote written to state/promote_log.jsonl in engine repo + <prod-AFS>/state/promote_log.jsonl in prod tenant AFS.12. Customer-facing surfaces now serve the new code.Sequence 3: Developer ships KLS data to prod
Section titled “Sequence 3: Developer ships KLS data to prod”1. `jin make numetrix.rmc` builds the KLS locally.2. `jin ship numetrix.rmc` runs.3. CLI resolves env: tenant says envs: [prod] → prod.4. CLI: "→ targeting numetrix.rmc on PROD (clerk.jinflow.io, jinflow-prod R2) This will refresh the customer-facing JinDesk. Confirm? [y/N]"5. Developer confirms.6. KLS uploaded to jinflow-prod R2.7. Fly machine refresh triggered on this-is-jinflow (later: jinflow-prod).8. New KLS visible to customers within ~30s.Sequence 4: Developer ships KLS data to dev
Section titled “Sequence 4: Developer ships KLS data to dev”1. Developer wants to test a UI change against rmc's data without affecting customers.2. `jin ship --env dev numetrix.rmc` runs.3. CLI: "→ targeting numetrix.rmc on DEV (intense-lionfish-66.accounts.dev, jinflow-dev R2)"4. KLS uploaded to jinflow-dev R2.5. dev.jinflow.io now serves a dev copy of rmc's KLS.6. No customer surface affected.7. Dev copy goes stale unless re-shipped; no automatic sync.Sequence 5: Non-developer tries jin ship —env dev
Section titled “Sequence 5: Non-developer tries jin ship —env dev”1. End-user analyst runs `jin ship --env dev numetrix.rmc`.2. CLI: "ERROR: dev environment is not configured for this CLI. This command is reserved for jazzisnow developers. ..."3. Operation refused. No remote call made.Or, more subtly:
1. End-user analyst runs `jin ship rmc` (no flag).2. CLI resolves env: tenant says envs: [prod] → prod.3. CLI proceeds without confirmation (end users don't get developer's prod-confirmation prompt).4. KLS shipped to prod. Same outcome as the developer's prod ship, minus the friction.Sequence 6: Developer accidentally tries to ship dev → prod KLS
Section titled “Sequence 6: Developer accidentally tries to ship dev → prod KLS”1. Developer ran `jin make numetrix.rmc --env dev` to build a test KLS.2. Forgets, runs `jin ship --env prod numetrix.rmc`.3. CLI: "→ targeting numetrix.rmc on PROD (clerk.jinflow.io, jinflow-prod R2) This will refresh the customer-facing JinDesk. Confirm? [y/N]"4. Developer notices the PROD target, cancels.5. Re-runs `jin ship --env dev numetrix.rmc` to land in dev as intended.6. No customer harm.The prod confirmation step is the safety net for exactly this case.
Migration path — adopting Sense 48 from current state
Section titled “Migration path — adopting Sense 48 from current state”Roughly the order the gated tasks (#27–#32, plus #25 R2 reorg, #21 proxy move, #22 prod rename) should land:
| Phase | Step | Tasks involved | Risk |
|---|---|---|---|
| 1 | Add envs: [prod] to all existing tenant.yml | (any) | none — no behaviour change |
| 2 | Restructure secrets.yml to dual-keys schema (clerk.prod, clerk.dev); migrate existing clerk_publishable_key → clerk.prod.publishable_key with backward-compat read | #30 | low — CLI gracefully reads legacy + new |
| 3 | Implement CLI env-resolution helper (resolve_tenant_env(tenant) -> 'dev'|'prod') | (foundation) | low |
| 4 | Make jin invite, jin sync-principals, jin revoke env-aware | #31 | low — flag-free for single-env tenants |
| 5 | Make jin ship env-aware; require #25 (jinflow-prod R2 bucket exists) | #32 | medium — touches customer data path |
| 6 | About page dev/prod indicator + DEV ELEVATED badge | #27 | trivial |
| 7 | platform_role enforcement on prod Clerk + server-side capability check | #29 | medium — auth-path change |
| 8 | just prod-promote + smoke test runner + dual audit log | #28 | medium — new verb, audit-critical |
| 9 | Restructure jin login to support --env dev and dual session cache | #30 cont’d | low |
| 10 | UI: prod confirmation prompts for developers | (part of #27/#30) | trivial |
Each phase is self-contained. Phase ordering allows landing 1-3 quickly (foundational, no user-visible change), then 4-6 to make env routing explicit, then 7-8 for elevated dev capabilities, then 9-10 for polish.
Integration with prior Senses
Section titled “Integration with prior Senses”-
Sense 25 (The Pass): env is orthogonal to identity. A user has the same principal_id across envs (by convention); what differs is which Clerk instance signs the token and which user table holds them. publicMetadata structure is identical across envs.
platform_roleis a new namespace on top of the existing tenant-scoped roles, additive not replacing. -
Sense 46 (The Roster): per-tenant principal_registry is per-env. A user invited to
numetrix.rmcon prod is NOT automatically a member ofnumetrix.rmcon dev (if dev has a copy). Each env has its own membership list.jin sync-principalsqueries the env’s Clerk and writes to the env’s AFS. -
Sense 29 (TBD — staff cross-tenant access):
platform_role: jazzisnow_devis the first concrete use of platform_role. Other staff roles (jazzisnow_support,jazzisnow_admin) become siblings in the same model. Sense 48 doesn’t fully define platform_role; Sense 29 does. -
Sense 43 (The Cascade): env is not a cascade dimension — it’s an orthogonal selector. The cascade (engine → pack → tenant → entity → instant) resolves config; env routes resolved config to the right Clerk/bucket/Fly app. A future implementation might want env-aware cascade overrides (e.g. dev-only debug settings), but that’s deferred.
Non-goals (explicit)
Section titled “Non-goals (explicit)”- KLS does not promote between buckets. Customer data lives in one bucket per env. There is no data-promote command (and
just prod-promoteonly ever moves JinDesk image, never the KLS). Period. - No session state. No
jin env devcommand that sets a “current env.” The tenant carries the env; the session does not. - No cross-env Clerk user migration. Dev users and prod users are independent. Signing in to dev does not create a prod identity (and vice versa). A developer has two separate Clerk users, one in each app, even if the
principal_idconvention links them. - No silent env detection from URL. A developer running
jin ship numetrix.rmcdoes not get env auto-detected from “the URL the tenant currently lives at.” Env always comes fromtenant.ymldeclaration or--envflag. URL inference would be subtly different (URL might be cached, stale, or under migration). - Not yet — env-scoped pack visibility. Some future use case might want certain packs visible only in dev (experimental analyses). Deferred to a later Sense; for now, all packs visible in all envs they’re installed in.
Resolved decisions (Mig 2026-06-14)
Section titled “Resolved decisions (Mig 2026-06-14)”- tenant.yml schema: always
envs:as a list, no bare-string form. just prod-promotevalidation: smoke-test gate REQUIRED;--force-no-smokeis an emergency escape recorded separately.- platform_role interaction:
jazzisnow_develevates (additive to per-tenant capabilities) with explicit UI surfacing (DEV ELEVATEDbadge) and CLI command acknowledgment (target-env one-liner + prod confirmation step). - promote_log.jsonl location: both — engine repo (
state/promote_log.jsonl) for developer audit, plus prod AFS (<prod-AFS>/state/promote_log.jsonl) for operations audit. - Developer flags: NO mandatory
--dev/--prodon every command. Tenant carries env; flags are needed only for multi-env tenants or non-tenant-scoped commands (jin login,jin explore). The asymmetric model means end users never see flags; developers see them when relevant.
Still open for implementation
Section titled “Still open for implementation”- Exact field set for the smoke-test attestation record (see proposed schema in “Promote requires a smoke-test gate” — to be confirmed during implementation).
- Mechanism for triggering smoke runs — manual
jin smoke, post-ship hook, scheduled CI? Probably all three, but the default trigger after deploy needs to be settled. - Per-env tenant slug collisions in CLI output: if
numetrix.rmclives in both dev and prod with different content, every CLI output should clearly state the env. Pattern proposed: prefix-suffixnumetrix.rmc[prod]/numetrix.rmc[dev]in any list output where both could appear. - How
just prod-promotehandles rollback. Probably: keep N previous prod image tags in Fly’s image history;jin rollbackredeploys the previous tag. Mechanism is straightforward; policy (how far back can you roll? does rollback require smoke too?) needs settling. - Whether
JINFLOW_ADMIN_TOKEN(the existing escape hatch) deserves a per-env split as well. Probably yes — admin tokens shouldn’t cross env boundaries any more than Clerk keys should.
Status of this Sense: full spec drafted 2026-06-14, awaiting review. Once reviewed and committed, this becomes the gating reference for tasks #27, #28, #30, #31, #32, plus the dependent operational tasks (#21, #22, #25) that complete the dev/prod topology.
Numerical neighbors: ← Sense 47: The Log — the system’s write-once memory · Sense 49 — The Branch →