Skip to content

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.md in 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.

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 the jazzisnow org, serving dev.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.

TermMeaning in this Sense
envA namespace value, either dev or prod. Carries through auth, app, data, and membership planes. Declared per-tenant in tenant.yml.
developerA jazzisnow staff member with platform_role: jazzisnow_dev on their prod Clerk publicMetadata. Distinct from any per-tenant role they may hold.
end userAny user who is not a developer — customers, analysts, support consumers. The vast majority of users.
elevatedOperating with a platform_role. The CLI and JinDesk surface this explicitly when active.
prod-implicitThe CLI’s default mode for end users: every command targets prod without flags.
opt-in devThe CLI’s default mode for developers: prod is still implicit per-tenant, but dev capabilities become reachable through --env flags and explicit configuration.

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 --env flag.
  • 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.

PlaneWhat env determinesConcrete instances today
AuthWhich Clerk app signs tokens, manages users, hosts sign-inprod: clerk.jinflow.io ; dev: intense-lionfish-66.accounts.dev
AppWhich Fly app’s JinDesk image is runningprod: this-is-jinflow (becomes jinflow-prod) ; dev: jinflow-dev
DataWhich R2 bucket holds the KLS filesprod: jinflow-prod (TBD, replacing jinflow-demo, EU jurisdiction) ; dev: jinflow-dev (already EU)
MembershipWhich principal_registry the cloud JinDesk checks, materialized from which Clerk’s publicMetadataper 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.

Each Clerk app has its own:

  • Frontend API hostclerk.jinflow.io (prod) vs clerk.intense-lionfish-66.accounts.dev (dev).
  • Account Portalaccounts.jinflow.io (prod) vs accounts.intense-lionfish-66.accounts.dev (dev).
  • Publishable keypk_live_… (prod) vs pk_test_… (dev), both decoding to their respective Frontend API hosts.
  • Secret keysk_live_… (prod) vs sk_test_… (dev).
  • User table — independent. A user signing up on dev does not exist on prod.
  • JWT templatescli and jinflow-launcher are configured identically on both (same name, same aud, 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.py validation 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 to secrets.yml).

Each Fly app holds a deployed JinDesk image. Today:

Fly appOrgOrg-canonical name (post-rename)Hostnames
this-is-jinflowjazzisnowjinflow-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-devjazzisnow(already canonical)dev.jinflow.io, jinflow-dev.fly.dev
jinflow-proxypersonal → 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=prod on jinflow-prod, JINFLOW_ENV=dev on jinflow-dev.
  • Clerk keys differ (prod vs dev).
  • R2_BUCKET differs (jinflow-prod vs jinflow-dev).
  • JINFLOW_ADMIN_TOKEN differs (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, alongside just ship and just dev-deploy, not on jin (which is reserved for tenant data operations — jin make, jin ship, jin explore). The original spec proposed jin promote; that drifted into just prod-promote once we sharpened the line (2026-06-23). The underlying logic still lives in jinflow/cli/commands/promote.py and remains testable as a module; the just recipe just invokes it via python3 -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_ENV env var → about page shows wrong badge; CLI confirmation prompts misfire. Not a security issue but confusing.

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 for envs: [prod] tenants (rmc, vai, numetrix, hrcentral, millesime).
  • jinflow-dev holds KLS files for envs: [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-dev on jinflow-prod Fly 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.

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: mig

A 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-principals updates from current Clerk; if user removed themselves from publicMetadata, principal is archived.

Only app artifacts promote. KLS data never does.

  • jin ship writes 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 on jinflow-dev, retags it, and deploys it to jinflow-prod. Bit-identical — no rebuild between dev validation and prod deployment.

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/):

  1. Sign-in flow — open dev.jinflow.io/sign-in, complete a headless Clerk OAuth round-trip against the dev Clerk app, end up at /admin with a valid __session cookie.
  2. KLS load — request /numetrix.rmc/dimensions/cases (or equivalent) against a known dev fixture tenant, expect a 200 with the expected row count tolerance.
  3. Health endpointGET /health returns 200.
  4. Schema-version round-trip — request the schema endpoint, verify it matches the image’s expected schema SHA.
  5. 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.

Every promote writes to two logs:

  • state/promote_log.jsonl in 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.jsonl in 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"}

Per-tenant declaration in tenant.yml, always a list:

envs: [prod] # single-env tenant (rmc, vai, customer tenants)
# OR
envs: [dev, prod] # multi-env tenant (development copies, staging fixtures)
# OR
envs: [dev] # dev-only test fixture

Default 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.

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.inspire as 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.

Decision logic for every env-routing command:

  1. --env flag passed → use it (after authorization check). If user lacks dev access and --env dev is passed → refuse with the developer-mode-not-configured error.
  2. 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 dev or --env prod”.
  3. Non-tenant-scoped commands (e.g. jin login) → use the command-specific default. For jin 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.

CommandDefault env source--env flag effectEnd-user visibility of flag
jin invite <email> <tenant>tenant’s envs: (single-env) or error (multi-env)Override; require dev access for --env devHidden if no dev keys present
jin revoke <user> <tenant>samesameHidden if no dev keys present
jin sync-principals <tenant>samesameHidden if no dev keys present
jin ship <tenant>samesameHidden if no dev keys present
jin make <tenant>(no env interaction — runs locally against AFS)n/an/a
jin loginprodAdds a dev session (does not replace prod); --env both runs bothHidden if no dev keys present
jin logoutall sessionsClears only the specified sessionHidden if no dev keys present
jin explore <tenant>always dev (safety)--env prod opts into prod Clerk for the local launchVisible if dev keys present; hidden otherwise
just prod-promotealways (dev image → prod Fly app); not env-routing in the per-tenant sensen/aHidden if not jazzisnow_dev
jin smokealways dev (runs against jinflow-dev)n/aHidden 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:

LayerSignalWhat it gatesWhere it lives
L1 — local secretsDev 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 configInferred from L1, or explicit developer: true in configCLI surfaces dev affordances in interactive prompts~/.jinflow/config.yml (optional)
L3 — cloud authorizationplatform_role: jazzisnow_dev in user’s prod Clerk publicMetadataServer-side enforcement — dev JinDesk + dev Clerk reject requests from users without this roleClerk 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 ship keys on deploy_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 the deploy_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 of deploy_token (env var JINFLOW_DEPLOY_TOKEN or ~/.jinflow/secrets.yml). The layered model above still applies to jin 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.

JinDesk shows a small badge in the page header indicating environment:

  • prod → no badge (the default, invisible to end users).
  • dev → yellow DEV chip with the tenant slug, always visible.
  • local-launcher → a separate indicator showing LOCAL, 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.

┌─────────────────────────────────────────────────────────────────┐
│ jinflow numetrix.rmc ⚙ Mig▼ [DEV ELEV] │
│ [DEV] │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ jinflow numetrix.rmc ⚙ Ronnie ▼│
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 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 ship to prod
  • jin invite to prod
  • jin revoke from prod
  • jin sync-principals against 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 effect
  • jin login — interactive flow has its own confirmation built in
  • jin 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 shape

Per-command session selection:

  1. Resolve env from tenant (or flag).
  2. Read <env>.token.
  3. If expired or missing → prompt to run jin login --env <env>.
  4. 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.

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-in
2. 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.io
4. User authenticates (Google OAuth)
5. Clerk returns __session cookie scoped to .jinflow.io
6. User lands at /numetrix.rmc dashboard
7. 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 data

No dev anywhere in this sequence. The user has no idea dev exists.

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:

PhaseStepTasks involvedRisk
1Add envs: [prod] to all existing tenant.yml(any)none — no behaviour change
2Restructure secrets.yml to dual-keys schema (clerk.prod, clerk.dev); migrate existing clerk_publishable_keyclerk.prod.publishable_key with backward-compat read#30low — CLI gracefully reads legacy + new
3Implement CLI env-resolution helper (resolve_tenant_env(tenant) -> 'dev'|'prod')(foundation)low
4Make jin invite, jin sync-principals, jin revoke env-aware#31low — flag-free for single-env tenants
5Make jin ship env-aware; require #25 (jinflow-prod R2 bucket exists)#32medium — touches customer data path
6About page dev/prod indicator + DEV ELEVATED badge#27trivial
7platform_role enforcement on prod Clerk + server-side capability check#29medium — auth-path change
8just prod-promote + smoke test runner + dual audit log#28medium — new verb, audit-critical
9Restructure jin login to support --env dev and dual session cache#30 cont’dlow
10UI: 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.

  • 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_role is 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.rmc on prod is NOT automatically a member of numetrix.rmc on dev (if dev has a copy). Each env has its own membership list. jin sync-principals queries the env’s Clerk and writes to the env’s AFS.

  • Sense 29 (TBD — staff cross-tenant access): platform_role: jazzisnow_dev is 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.

  • KLS does not promote between buckets. Customer data lives in one bucket per env. There is no data-promote command (and just prod-promote only ever moves JinDesk image, never the KLS). Period.
  • No session state. No jin env dev command 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_id convention links them.
  • No silent env detection from URL. A developer running jin ship numetrix.rmc does not get env auto-detected from “the URL the tenant currently lives at.” Env always comes from tenant.yml declaration or --env flag. 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.
  1. tenant.yml schema: always envs: as a list, no bare-string form.
  2. just prod-promote validation: smoke-test gate REQUIRED; --force-no-smoke is an emergency escape recorded separately.
  3. platform_role interaction: jazzisnow_dev elevates (additive to per-tenant capabilities) with explicit UI surfacing (DEV ELEVATED badge) and CLI command acknowledgment (target-env one-liner + prod confirmation step).
  4. 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.
  5. Developer flags: NO mandatory --dev/--prod on 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.
  • 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.rmc lives in both dev and prod with different content, every CLI output should clearly state the env. Pattern proposed: prefix-suffix numetrix.rmc[prod] / numetrix.rmc[dev] in any list output where both could appear.
  • How just prod-promote handles rollback. Probably: keep N previous prod image tags in Fly’s image history; jin rollback redeploys 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

jazzisnow jinflow is a jazzisnow product
v0.64.7 · built 2026-09-20 19:48 UTC