Skip to content

Sense 16: P2P2P — Peer-to-Peer-to-People

Sense 16 · Steady · Last touched 2026-08-23

  • last_verified: 2026-04-10

Synced from docs/design/sense_16_p2p2p.md in the engine repo — that’s the source; this page is a build-time mirror.

Your data. Your machine. Their browser.

The safest place for sensitive data is the machine that owns it. The best place for JinDesk is where people can reach it. P2P2P connects the two without moving the data.

Status: proposed (infrastructure exists, protocol needs formalisation) Author: the owner + Claude (conversation, 2026-04-07)


Healthcare data is sensitive. Material costs, billing patterns, case volumes — these are operational facts that hospitals guard with legal weight. Uploading a KLS to someone else’s cloud is, for many organisations, simply not an option.

But analysis is a collaborative act. The data engineer builds the KLS. The consultant reviews findings. The CFO wants the headline numbers on their iPad. The domain expert annotates anomalies. These people are rarely in the same room, on the same network, or willing to install software.

Today jinflow offers three deployment modes:

  1. Localjinflow explore on the same machine. Private, fast, lonely.
  2. Cloud (R2) — KLS uploaded to Cloudflare R2, served by Fly.io. Collaborative, but the data leaves the site.
  3. Proxyjinflow-proxy --tunnel. Data stays local, JinDesk in the cloud. A one-off session.

Mode 3 is the seed of something bigger. It already does the hard parts: Cloudflare Tunnel punches through NAT, the proxy serves queries read-only, and the cloud JinDesk renders the UI. The data never leaves the owner’s machine.

P2P2P is Mode 3 formalised as the primary deployment model.


Hospital West Hospital East
┌──────────────────┐ ┌──────────────────┐
│ jinflow-proxy │ │ jinflow-proxy │
│ KLS: rmc_west │ │ KLS: rmc_east │
│ Port 7654 │ │ Port 7654 │
│ ──── tunnel ──── │ │ ──── tunnel ──── │
└────────┬─────────┘ └────────┬─────────┘
│ │
│ Cloudflare Network │
│ │
┌────────┴────────────────────────────────────┴────────┐
│ │
│ JinDesk (proxy.jinflow.io) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Proxy Directory │ │
│ │ rmc_west ● online (tunnel → west) │ │
│ │ rmc_east ● online (tunnel → east) │ │
│ │ rmc_apex ○ offline (last seen 2h ago) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└──────────┬──────────────────────────────┬─────────────┘
│ │
┌────┴────┐ ┌────┴────┐
│ CFO │ │ Analyst │
│ iPad │ │ Laptop │
└─────────┘ └─────────┘

No cloud storage. No data replication. No bucket policies. Each hospital runs a proxy. JinDesk is a query router.


ComponentStatusWhat it does
jinflow-proxyshippedOpens KLS read-only, serves SQL over HTTP, token auth
--tunnel flagshippedStarts Cloudflare Tunnel, gets public URL
--explorer registrationshippedRegisters with cloud JinDesk, gets session URL
proxyRegistry.tsshippedIn-memory directory of active proxies
/api/proxy-registershippedRegister/deregister endpoint
/s/[token] routeshippedSession entry: sets cookie, redirects to tenant
hooks.server.ts proxy routingshippedRoutes queries to proxy URL via HTTP fetch
Deregistration on shutdownshippedCtrl+C → deregister → clean exit

The machinery is built. What’s missing is the protocol and the trust model.


jinflow-proxy /path/to/kls.duckdb --tunnel --explorer https://proxy.jinflow.io
  1. Proxy opens KLS read-only
  2. Proxy starts HTTP server on localhost:7654
  3. Proxy starts Cloudflare Tunnel → gets https://xxx.trycloudflare.com
  4. Proxy calls POST /api/proxy-register on JinDesk:
    {
    "action": "register",
    "tenant": "rmc_west",
    "pack": "numetrix",
    "proxy_url": "https://xxx.trycloudflare.com",
    "token": "abc123...",
    "schemas": ["hospital_zeta"],
    "size_mb": 89
    }
  5. JinDesk stores the proxy in the directory, returns a session URL
  6. Proxy prints the session URL — share it with people who should have access
Browser → JinDesk → POST proxy_url/query { sql, params } → DuckDB → JSON → JinDesk → Browser

JinDesk never stores query results. It’s a pass-through.

On Ctrl+C or SIGTERM:

  1. Proxy calls POST /api/proxy-register with "action": "deregister"
  2. JinDesk removes the proxy from the directory
  3. Tunnel closes
  4. All sessions for that tenant become inactive

Currently missing. Without it, a proxy that crashes or loses network silently becomes a dead entry in the directory. Proposed:

  • Proxy sends POST /api/proxy-heartbeat every 60 seconds
  • JinDesk marks proxy as stale after 3 missed heartbeats (3 minutes)
  • Stale proxies are removed from the directory
  • Tenant picker shows “last seen” timestamp for offline proxies

Data sovereignty means: the data owner decides, moment by moment, whether their data is accessible. The decision is physical, not administrative. You don’t revoke a permission — you unplug.

In P2P2P:

WhatWhereWho controls it
KLS fileOwner’s machineOwner
Query executionOwner’s machineOwner
Query resultsIn transit (TLS)Cloudflare (transport)
JinDesk UICloud (Fly.io)jinflow
Proxy directoryCloud (Fly.io, in-memory)jinflow
Session tokenOwner’s machine + browser cookieOwner + user

The data never leaves the owner’s machine. Query results transit through Cloudflare’s network (TLS-encrypted) and JinDesk server (pass-through, not stored), then reach the user’s browser.

ThreatMitigationResidual risk
Unauthorised query accessToken-based auth (24-char random, per session)Token leak → data access. Mitigate: header-only auth (see recommendations)
Bulk exfiltrationKLS is read-only; SQL whitelist (SELECT/WITH/SHOW/DESCRIBE/PRAGMA only)Token holder can SELECT * every table. Mitigate: result size limits, query audit log
Man-in-the-middleTLS everywhere (Cloudflare Tunnel = HTTPS, JinDesk = HTTPS)Cloudflare is the TLS terminator — they could theoretically inspect traffic
JinDesk server compromiseJinDesk is pass-through, stores no data. Proxy directory is in-memory (lost on restart)Attacker could redirect queries to a malicious proxy. Mitigate: proxy URL pinning
Proxy process compromiseDuckDB opened read-only. SQL whitelist blocks writesAttacker with local access has bigger problems than the proxy
Tunnel URL guessableCloudflare Quick Tunnels use random subdomains (xxx.trycloudflare.com)Not cryptographically random. For production: use named tunnels with authentication
Session token in URLCurrently supported via query param (?token=)Leaks to browser history, logs, referrer headers. Mitigate: header-only auth
Stale proxy in directoryDeregistration on shutdownCrash or network loss → stale entry. Mitigate: heartbeat protocol
CORS wildcardCurrent: Access-Control-Allow-Origin: *Any website knowing the token can query the proxy. Mitigate: restrict to JinDesk origin
SIS auto-attachSIS file next to KLS is silently attached (read-only)SIS may contain notebook data the owner didn’t intend to expose. Mitigate: explicit --with-sis flag

Security Recommendations (for production use)

Section titled “Security Recommendations (for production use)”

Must-have (before real deployments):

  1. Header-only authentication — remove query param token support (?token=). Tokens must only travel in Authorization: Bearer headers. This prevents leakage to browser history, server logs, and referrer headers.

  2. CORS origin restriction — replace Access-Control-Allow-Origin: * with the specific JinDesk origin (https://proxy.jinflow.io). This prevents third-party websites from querying the proxy even if they obtain the token.

  3. Result size limits — add a configurable --max-rows flag (default: 10,000). Queries returning more rows are truncated with a warning. This limits the blast radius of bulk exfiltration.

  4. Explicit SIS opt-in — require --with-sis flag to attach the SIS file. Default: KLS only. The data owner should consciously decide whether notebook data is exposed.

  5. Heartbeat protocol — proxy sends periodic heartbeats to the JinDesk. JinDesk removes stale entries after timeout. This prevents ghost sessions in the directory.

Should-have (for multi-tenant / enterprise):

  1. Query audit log — log every query (SQL, timestamp, token hash, response time, row count) to a local file. The data owner has a complete record of who queried what.

  2. Named Cloudflare Tunnels — replace Quick Tunnels (trycloudflare.com) with named tunnels authenticated to a Cloudflare account. This gives stable URLs, access policies, and audit trails on Cloudflare’s side.

  3. Token rotation — allow --token-ttl to auto-rotate the session token. When the token rotates, the proxy re-registers with the JinDesk. Active sessions get a new cookie transparently.

  4. IP allowlist--allow-ip flag to restrict which IPs can query the proxy (in addition to token auth). Defence in depth.

  5. Proxy identity — sign registration requests with a key pair. JinDesk can verify that a proxy claiming to be “rmc_west” is actually from RMC West, not an impersonator.

Nice-to-have (for polish):

  1. Bandwidth metering — track bytes served per session. Display in the proxy console and optionally enforce a cap.

  2. Query complexity limits — reject queries with excessive JOINs, subqueries, or estimated cost (DuckDB’s EXPLAIN can estimate this).

  3. Session expiry--session-ttl 4h to auto-shutdown the proxy after a time window. For scheduled review sessions.


For the data owner (hospital IT, data engineer)

Section titled “For the data owner (hospital IT, data engineer)”
Terminal window
# Build the KLS as usual
jinflow make numetrix.rmc_west
# Share it — one command
jinflow-proxy store/numetrix_rmc_west_kls.duckdb --tunnel
# Output:
# jinflow proxy
#
# KLS: numetrix_rmc_west_kls.duckdb (89 MB)
# Tenant: rmc_west
# Pack: numetrix
# Schemas: hospital_zeta
#
# Local: http://localhost:7654
# Token: k8m2np4qr7st9vwx...
# Tunnel: https://amber-fox-23.trycloudflare.com
#
# Registered with proxy.jinflow.io
# Open: https://proxy.jinflow.io/s/k8m2np4qr7st9vwx
#
# Press Ctrl+C to stop

Share the “Open” URL with anyone who should have access. When you’re done, press Ctrl+C. The data disappears from the cloud instantly.

  1. Receive a URL: https://proxy.jinflow.io/s/k8m2np4qr7st9vwx
  2. Click it — JinDesk opens with the tenant’s data
  3. Browse findings, hypotheses, entities — the full JinDesk experience
  4. Every query is answered in real-time from the owner’s machine
  5. No login, no account, no installation

For the administrator (multi-site coordinator)

Section titled “For the administrator (multi-site coordinator)”

The tenant picker at proxy.jinflow.io shows all registered proxies:

┌─────────────────────────────────────────────────┐
│ jinflow Explorer │
│ │
│ Online tenants: │
│ ● numetrix / rmc_west 89 MB 2m ago │
│ ● numetrix / rmc_east 67 MB 1m ago │
│ ● millesime / zufferey 12 MB 5m ago │
│ │
│ Recently offline: │
│ ○ numetrix / rmc_apex 45 MB 2h ago │
│ │
└─────────────────────────────────────────────────┘

Click any online tenant to enter. No credentials beyond the session URL.


Cloud (R2)P2P2P
Data locationCloudflare R2 bucketOwner’s machine
Data at restEncrypted (bucket policy)Owner’s filesystem
Access controlIAM policies, bucket ACLsPhysical: proxy on/off
RevocationDelete from bucket, purge cachesCtrl+C
Revocation latencyMinutes (propagation)Instant
Compliance auditCloud provider logsLocal query audit log
Availability24/7 (cloud SLA)Only when proxy runs
Latency~50ms (edge cache)~200-500ms (tunnel round-trip)
CostStorage + egressZero (Cloudflare Quick Tunnels are free)
SetupR2 credentials, sync profiles, bucket configOne command

The tradeoff is availability for sovereignty. P2P2P is not always-on. It’s on when the owner decides it’s on. For scheduled reviews, audits, and consulting sessions, that’s not a limitation — it’s the feature.


In P2P2P mode, JinDesk stores nothing:

  • No KLS files
  • No query results
  • No user sessions (beyond httpOnly cookies pointing to proxy tokens)
  • No credentials

The proxy directory is in-memory. A restart wipes it clean. Proxies re-register within 60 seconds (heartbeat).

This makes JinDesk horizontally scalable and disposable. You can run it on a free-tier Fly.io instance. If it crashes, restart it. Proxies reconnect automatically.

Every proxy serves the same interface: POST /query { sql }. The JinDesk doesn’t care whether the KLS is local, on R2, or behind a tunnel. The query routing in hooks.server.ts already handles all three modes.

This means P2P2P is not a special mode — it’s the same mode as cloud, with a different transport. The code paths converge.

Nothing prevents:

  • One machine serving multiple KLS files (run multiple proxy instances on different ports)
  • Multiple machines serving tenants from the same pack
  • A mix of cloud-hosted and proxy-served tenants in the same JinDesk

The tenant picker already shows both. A consultant could see three hospitals: two online via proxy, one cached from R2. Same UI, same queries, different data paths.


P2P2P: Peer-to-Peer-to-People.

The first P2P is the machine-to-machine connection (proxy ↔ JinDesk). The second P is the human at the end — the person who needs to see the data without touching the infrastructure.

Traditional P2P moves data between peers. P2P2P moves insight between peers and delivers it to people.


Phase 1: Harden (current proxy, production-ready)

Section titled “Phase 1: Harden (current proxy, production-ready)”
  • Header-only auth (remove query param token)
  • CORS origin restriction
  • Result size limits (--max-rows)
  • Explicit SIS opt-in (--with-sis)
  • Query audit log (local file)
  • Heartbeat protocol (60s interval, 3min timeout)

Phase 2: Directory (multi-proxy awareness)

Section titled “Phase 2: Directory (multi-proxy awareness)”
  • Persistent proxy directory (survive JinDesk restarts)
  • Tenant picker shows online/offline status
  • “Last seen” timestamps for offline proxies
  • Auto-reconnect after tunnel interruption
  • Named Cloudflare Tunnels (stable URLs, access policies)
  • Proxy identity (key pair signing)
  • Token rotation (--token-ttl)
  • IP allowlist (--allow-ip)
  • Session expiry (--session-ttl)
  • A proxy can register with multiple JinDesks simultaneously
  • JinDesks can cross-reference each other’s directories
  • Federated search: “find all online tenants with pack=numetrix”

P2P2P operates in a spectrum of trust:

The session URL works without login. Click and browse. No account, no credentials, no trace of who you are. This is not a compromise — it’s a trust-building feature.

For the inexperienced user — the CFO who’s never used an analytics platform, the department head who’s skeptical about “yet another system” — the absence of a login wall is an invitation. “Just look. No commitment.” They see the findings, the hypotheses, the executive summary. They build confidence in the tool before anyone asks for their email.

Stealth mode is the lowest friction path to value. It answers: “What does this system do?” before asking “Who are you?”

When a viewer chooses to sign in (prompted by “Add to notebook” or “Save view”), they unlock:

  • Notebook access — add notes, discussion replies, action items
  • Bookmarks — saved views persist across sessions
  • Audit trail — their actions are recorded
  • Author attribution — their name appears on notes and annotations

The proxy owner controls whether identification is required:

Terminal window
jinflow-proxy kls.duckdb --tunnel # stealth: anyone with URL can view
jinflow-proxy kls.duckdb --tunnel --require-auth # identified: GitHub sign-in required

Default is stealth. The data owner opts into identification when they need audit-grade sessions (e.g. regulatory reviews, consulting engagements).

This is the same identity model across all deployment modes:

ModeAuthNotebookAudit
Local (jinflow explore)None (god mode)Full accessOptional
Cloud (R2)Demo read-only / GitHub authAuth required for writesYes
P2P2P stealthNoneRead-onlyNo
P2P2P identifiedGitHub authFull accessYes

The capabilities system (Sense 12) handles all four. The code paths converge — same requireCapability(), same DEMO_CAPABILITIES, same UserBadge. The only variable is how the identity is established.


Authenticating the Local Launcher (Sense 25 + loopback redirect)

Section titled “Authenticating the Local Launcher (Sense 25 + loopback redirect)”

Your sign-in. Their auth server. Your callback.

The sovereignty principle generalises. The original framing kept data local while the JinDesk rendered in the cloud (“your data, your machine, their browser”). The same shape, applied to identity, keeps the auth provider in the cloud while the session lives on the local machine.

This section is the integration between Sense 16 (P2P2P) and Sense 25 — Identity & Passes for the local-launcher case (jinflow explore on the owner’s laptop).

Why the cloud-style flow doesn’t transfer

Section titled “Why the cloud-style flow doesn’t transfer”

In cloud-JinDesk mode (rmc.jinflow.io, this-is.jinflow.io), the SvelteKit app mounts <ClerkProvider> and the Clerk JavaScript SDK talks to Clerk’s Frontend API directly. Clerk accepts the request because the page’s Origin header matches the production domain that the owner whitelisted in the Clerk Dashboard (the production Clerk domain for jinflow itself is TBD; for the examples below, read <prod-domain> as whatever that ends up being).

The same code path on localhost:<any port> fails by Clerk’s deliberate design: production keys are domain-locked, and “every desktop on the planet” is not whitelistable. Clerk’s response on the failed request is explicit:

Production Keys are only allowed for domain “<prod-domain>”. The Request HTTP Origin header must be equal to or a subdomain of the requesting URL.

Whitelisting localhost:<port> is also not a fix:

  • Free tier — only one domain (the Primary). No extra origins permitted at all.
  • Pro tier — Satellites allow additional origins, but each one is exact-match. End users pick whatever port jinflow explore binds to; whitelisting every combination is unworkable.
  • Wildcard ports — Clerk does not support localhost:* as an allowed origin. The CORS check is exact.

So embedding Clerk in the local app is a dead end for a distributable artifact. The architecture has to change shape, not just configuration.

This is the OAuth pattern every distributed desktop app uses (gh CLI, GitHub Desktop, VS Code, Slack, Spotify, 1Password CLI, …). The local app never embeds the auth provider’s UI. It opens the system browser pointed at the provider’s hosted sign-in page, and catches a redirect back to a localhost URL once the user has authenticated there.

jinflow explore on localhost:<port> <prod-domain> (Clerk-allowed origin)
┌────────────────────────────────────┐ ┌──────────────────────────────────┐
│ 1. Gate fires for /numetrix.rmc/… │ │ │
│ /sign-in route is hit. │ │ │
│ │ │ │
│ 2. /sign-in DOES NOT mount the │ │ │
│ Clerk SDK. It opens the system │──────▶│ 3. Clerk's hosted sign-in widget │
│ browser: │ │ renders. User signs in. │
│ https://accounts.<prod-domain>/ │ │ │
│ sign-in? │ │ 4. Clerk redirects: │
│ redirect_url= │ │ http://localhost:<port>/ │
│ http://localhost:<port>/ │ │ auth-callback?ticket=… │
│ auth-callback │◀──────│ │
│ │ │ │
│ 5. /auth-callback route receives │ │ │
│ the ticket, exchanges it for a │ │ │
│ __session cookie via Clerk's │ │ │
│ backend API (sk_live_…). │ │ │
│ │ │ │
│ 6. Redirects to /numetrix.rmc/… │ │ │
└────────────────────────────────────┘ └──────────────────────────────────┘

Two halves of the same identity, two different surfaces:

  • The publishable key (pk_live_…) lives only on the production domain’s pages — where Clerk has already approved the origin.
  • The secret key (sk_live_…) lives only on the local machine’s server process, never reaches a browser. It signs the ticket-exchange call from /auth-callback.

Why Clerk accepts this when it rejects the embedded flow

Section titled “Why Clerk accepts this when it rejects the embedded flow”

Status note (2026-06-15): the loopback redirect described in this section is DEFERRED. During the prod-Clerk cutover we discovered Clerk’s Account Portal silently rejects redirect_url query parameters whose hostname is neither same-origin nor a subdomain of the application’s primary domain. http://localhost:<port> meets neither test. The fallback is the configured Home URL (jinflow.io apex marketing page) instead of returning to the local launcher. The Native-API “Allowlist for mobile SSO redirect” Mig found in the dashboard governs a different code path (mobile SDK OAuth redirects, not Account Portal). We tried a DNS subdomain trick (local.jinflow.io A → 127.0.0.1) so Clerk would treat the redirect as a subdomain of jinflow.io; Account Portal still rejected it in practice (its trust check is narrower than the docs describe).

Interim decision (committed 2026-06-15): local-launcher mode bypasses Clerk entirely. The auth gate in hooks.server.ts short-circuits when JINFLOW_LAUNCH_MODE === 'local-launcher', so page routes render without sign-in (“your machine, your data”). API routes keep their own Bearer check, so CLI commands (jin invite / jin sync-principals / jin revoke) still flow through the cloud JinDesk with proper auth — only the local JinDesk’s UI is auth-less.

The local.jinflow.io DNS records remain in place as a building block in case we revisit this design. The deriveHostedSignInUrl helper was retired from /sign-in/+page.server.ts (git history preserves it). The rest of this section describes the original design as it was authored before the cutover findings, retained for context and so future work can pick up the thread.

The constraint Clerk enforces is on Origin — the page making the SDK call. Redirect targets are a separate allow-list. The original design assumed Clerk (like most OAuth providers) accepts http://localhost and http://127.0.0.1 with any port as a redirect destination by default, per RFC 8252. In practice the Account Portal does not, for the reasons in the status note above.

So the configuration shape on the Clerk side was envisioned as:

Allow-listConfigured toEffect (envisioned)
Domains / Frontend Origins<prod-domain> onlyClerk SDK on cloud JinDesk works
Redirect URLs / Pathshttp://localhost + http://127.0.0.1Local launcher’s /auth-callback accepted

A future commit will realise this shape. The relevant surface:

PathChange
explorer/src/routes/+layout.svelte<ClerkProvider> mounts only when data.clerkPublishableKey && data.clerkOrigin === 'cloud'. On local-launcher mode it does not mount; no SDK on localhost.
explorer/src/routes/sign-in/+page.svelteBecomes a tiny redirector — opens https://accounts.<prod-domain>/sign-in?redirect_url=http://localhost:<port>/auth-callback in a new tab and shows a small “Complete sign-in in the browser tab” placeholder.
explorer/src/routes/auth-callback/+server.ts (new)Receives the ticket. Calls Clerk’s backend with CLERK_SECRET_KEY to exchange for a session token. Sets the __session cookie. Redirects to the original after_sign_in_url.
explorer/src/hooks.server.tsclerkHandle stays as-is for cloud. For local launches, signed-in state is established by the cookie from step 5 above; the per-request event.locals.auth() resolution works the same way Clerk’s own SDK does on the cloud side.
jinflow/cli/commands/launch.pySets a new env JINFLOW_LAUNCH_MODE=local-launcher (or cloud) so JinDesk knows which shape it’s in. The Clerk key passthrough that already shipped stays.
Clerk DashboardOnce: add http://localhost and http://127.0.0.1 under Paths → After sign-in URL / Redirect URLs. Production Domains stay as-is.

The user-visible flow becomes: “User downloads jinflow, runs jinflow explore, clicks Sign In, a browser tab opens at accounts.<prod-domain>, signs in, lands back in their local JinDesk signed-in.” No /etc/hosts setup, no per-machine configuration, no Clerk-plan upgrade required.

data.clerkLaunchMode (server-derived from JINFLOW_LAUNCH_MODE) is the single source of truth:

  • 'cloud' (Fly, future cloud-preview environments) → embedded Clerk SDK in the root layout, current pattern.
  • 'local-launcher' (jinflow explore on any machine, any port) → loopback redirect, hosted Clerk widget on the production domain.

The Sense 12 capabilities system stays unchanged. Once the __session cookie is set — whether by Clerk’s SDK on cloud or by the local-launcher’s /auth-callback — the rest of the stack treats the user identically.

The proxy-mode flow (“stealth, GitHub auth”) in the previous section is not superseded by this section — it’s a different deployment topology. Proxy mode runs JinDesk on a cloud host the owner already whitelisted, so Clerk SDK embedding works there the same way it works on rmc.jinflow.io. This section addresses only the case where the SvelteKit app itself runs on the owner’s laptop on localhost:<arbitrary>.

The offline guarantee — part of the contract, not an option

Section titled “The offline guarantee — part of the contract, not an option”

The sovereignty principle Sense 16 is built around requires that a signed-in owner can keep using their local JinDesk when the laptop has no internet. The loopback-redirect flow above gets us to a __session cookie on localhost; making that cookie survive offline is a deliberate implementation choice, not an implementation detail. Two non-negotiables:

  1. The __session cookie holds a long-lived JWT, minted via a Clerk JWT template (similar pattern to the existing cli template used by the CLI bearer flow). Default Clerk session tokens live ~60 seconds and rely on background SDK refresh to stay valid — that refresh requires network to Clerk. A template-minted JWT with a multi-day expiry (30 days, matching the cli template) decouples the cookie’s validity from connectivity. Sign in once with internet; stay signed in for the token’s lifetime regardless of network state afterwards.

  2. Signature verification happens offline. @clerk/backend’s verifyToken({ secretKey, audience }) verifies the JWT locally using cached JWKS (Clerk’s public signing keys) plus the secret key. The JWKS fetch is a one-shot at first online start; the cache survives across restarts. No request to Clerk is made during the verify path. The audience check we adopted for the cli template (aud: 'jinflow-cli') extends to this new template (aud: 'jinflow-launcher' or similar) by the same mechanism.

What this rules out:

  • Calling Clerk’s /v1/client/sessions/<sid>/tokens to refresh short-lived session tokens. The local launcher must never need this round-trip during normal operation.
  • Phoning Clerk for any per-request validation. The verify path is signature + audience + expiry, all local.

What this preserves:

  • The owner can sign in with internet on Monday morning, fly to a remote site with no connectivity, work in JinDesk all week, and re-sign-in only when the long-lived token expires. The experience matches a desktop application’s offline behaviour rather than a web app’s.
  • The cloud-JinDesk flow (rmc.jinflow.io, etc.) keeps using Clerk’s default SDK refresh as today; it has constant network to Clerk by definition.

This commitment also informs the JWT template naming: the existing cli template (for CLI Bearer auth) and the new launcher template (for cookie auth) are siblings, both 30-day expiry, both audience- checked, distinguishable by the aud claim alone. Same offline guarantee on both sides.


In P2P2P mode, the KLS lives on the proxy owner’s machine. The SIS (notes, bookmarks, audit) lives alongside it. But the SIS is mutable — remote viewers might want to add notes. Should those writes go to the proxy owner’s SIS?

Two concerns:

  1. Privacy asymmetry — the KLS is hospital data (sensitive). The SIS is user activity (less sensitive, but still personal). They have different risk profiles.
  2. Write routing — the proxy serves KLS read-only. SIS writes need a different path.

Option A: SIS on the proxy (owner’s machine)

Section titled “Option A: SIS on the proxy (owner’s machine)”

The SIS stays alongside the KLS. Remote SIS writes are routed through the proxy (which gains a /sis-write endpoint alongside /query).

Pro: All state in one place. Simple backup. The data owner controls everything. Con: The proxy becomes read-write. The owner may not want remote users writing to their filesystem. Requires --with-sis-write flag.

Each P2P2P session gets a cloud-side SIS — a lightweight DuckDB on the JinDesk server (or in Cloudflare D1). Notes and bookmarks live near the viewer, not near the data.

Pro: No writes to the proxy. The data owner’s machine stays read-only. SIS is less sensitive — it can live in the cloud without triggering compliance concerns. Con: The SIS is now separated from the KLS. When the proxy goes offline, the SIS is orphaned. Merge becomes harder.

  • KLS stays on the proxy (read-only, owner’s machine)
  • SIS lives on JinDesk (cloud-side, per-session or per-tenant)
  • Sync on demand: the proxy owner can pull the cloud SIS into their local SIS via jinflow sis pull — merging remote notes with local ones

This respects the different lifecycles:

  • KLS = sensitive hospital data → stays on-premise
  • SIS = user activity data → can live in the cloud
  • Merge = explicit, at the owner’s discretion
Proxy (owner's machine) JinDesk (cloud)
┌────────────────────┐ ┌────────────────────┐
│ KLS (read-only) │ ←queries→ │ UI rendering │
│ SIS (local) │ │ SIS (cloud) │
└────────────────────┘ └────────────────────┘
↑ ↑
│ jinflow sis pull │
└────────────────────────────────┘

The cloud SIS is not a permanent store — it’s a staging area. The owner decides when to merge it into their local SIS. Until then, the remote notes exist only in the cloud.


When a proxy goes offline (tunnel drops, owner closes laptop, network interruption), JinDesk should communicate this clearly:

JinDesk detects offline proxies via:

  1. Heartbeat timeout — no heartbeat for 3 minutes → mark stale
  2. Query failure — fetch to proxy URL fails → mark offline immediately

When the current tenant’s proxy goes offline:

┌──────────────────────────────────────────────────────────────────────┐
│ ⚠ This tenant is currently offline │
│ │
│ The data source (rmc_west) disconnected 2 minutes ago. │
│ The page shows the last loaded data — it may be stale. │
│ │
│ [Reconnecting automatically...] [Switch tenant] │
└──────────────────────────────────────────────────────────────────────┘
  • Pages already loaded stay visible (stale but useful)
  • New queries fail with a clear message (not a generic error)
  • Auto-reconnect — JinDesk polls the proxy URL every 15 seconds. When the proxy comes back, the banner disappears and data refreshes
  • Tenant picker shows offline tenants grayed out with “last seen”
  • No data loss — the viewer’s SIS (cloud-side) is unaffected by the proxy going offline. Notes written before disconnection are safe.

P2P2P is a deployment topology, not a UI mode — the same JinDesk runs locally on the data owner’s machine and serves remote viewers through a Cloudflare Tunnel. hooks.server.ts routes every SQL query through the tunnel back to the owner’s KLS; if the tunnel drops, the offline banner appears and the viewer’s session degrades gracefully. The KLS never leaves the owner’s machine; viewers’ notes land in the cloud SIS and reconcile back via Beat 3 of the Heartbeat.

  • Infrastructure: Cloudflare Tunnel routes browser → cloud JinDesk → owner’s local proxy → KLS, all over HTTPS
  • In the app: the SignedInBadge in JinDesk chrome gates SSR until Clerk auth completes (this is the Sense 25 + Sense 16 boundary) · the offline banner activates when the tunnel drops
  • Sovereignty: data presence is physical — Ctrl+C on the owner’s proxy vanishes the data from the internet instantly

Status: shipped (Mode 2 of the five operating modes per docs/design/operating_modes.md). Hardening (auth, capability gating, audit) is steady — Stealth Mode and the cross-mode pulse story are in the implementation roadmap above.


“Your hospital’s data never leaves your server room. But your CFO can browse findings on their iPad, your consultant can review from Zurich, and your auditor can verify from Bern — all at the same time, all seeing live data, all through a single URL. When you close the laptop, the data vanishes from the internet. Instantly.”

That’s P2P2P.


Numerical neighbors:Sense 15: Observation and Explanation · Sense 17: The House

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