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.mdin 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)
What pulled us in
Section titled “What pulled us in”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:
- Local —
jinflow exploreon the same machine. Private, fast, lonely. - Cloud (R2) — KLS uploaded to Cloudflare R2, served by Fly.io. Collaborative, but the data leaves the site.
- Proxy —
jinflow-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.
The Vision
Section titled “The Vision” 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.
What Already Exists
Section titled “What Already Exists”| Component | Status | What it does |
|---|---|---|
jinflow-proxy | shipped | Opens KLS read-only, serves SQL over HTTP, token auth |
--tunnel flag | shipped | Starts Cloudflare Tunnel, gets public URL |
--explorer registration | shipped | Registers with cloud JinDesk, gets session URL |
proxyRegistry.ts | shipped | In-memory directory of active proxies |
/api/proxy-register | shipped | Register/deregister endpoint |
/s/[token] route | shipped | Session entry: sets cookie, redirects to tenant |
hooks.server.ts proxy routing | shipped | Routes queries to proxy URL via HTTP fetch |
| Deregistration on shutdown | shipped | Ctrl+C → deregister → clean exit |
The machinery is built. What’s missing is the protocol and the trust model.
The P2P2P Protocol
Section titled “The P2P2P Protocol”Registration
Section titled “Registration”jinflow-proxy /path/to/kls.duckdb --tunnel --explorer https://proxy.jinflow.io- Proxy opens KLS read-only
- Proxy starts HTTP server on localhost:7654
- Proxy starts Cloudflare Tunnel → gets
https://xxx.trycloudflare.com - Proxy calls
POST /api/proxy-registeron JinDesk:{"action": "register","tenant": "rmc_west","pack": "numetrix","proxy_url": "https://xxx.trycloudflare.com","token": "abc123...","schemas": ["hospital_zeta"],"size_mb": 89} - JinDesk stores the proxy in the directory, returns a session URL
- Proxy prints the session URL — share it with people who should have access
Query Flow
Section titled “Query Flow”Browser → JinDesk → POST proxy_url/query { sql, params } → DuckDB → JSON → JinDesk → BrowserJinDesk never stores query results. It’s a pass-through.
Deregistration
Section titled “Deregistration”On Ctrl+C or SIGTERM:
- Proxy calls
POST /api/proxy-registerwith"action": "deregister" - JinDesk removes the proxy from the directory
- Tunnel closes
- All sessions for that tenant become inactive
Heartbeat (proposed)
Section titled “Heartbeat (proposed)”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-heartbeatevery 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
Privacy Architecture
Section titled “Privacy Architecture”The Sovereignty Principle
Section titled “The Sovereignty Principle”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:
| What | Where | Who controls it |
|---|---|---|
| KLS file | Owner’s machine | Owner |
| Query execution | Owner’s machine | Owner |
| Query results | In transit (TLS) | Cloudflare (transport) |
| JinDesk UI | Cloud (Fly.io) | jinflow |
| Proxy directory | Cloud (Fly.io, in-memory) | jinflow |
| Session token | Owner’s machine + browser cookie | Owner + 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.
Threat Model
Section titled “Threat Model”| Threat | Mitigation | Residual risk |
|---|---|---|
| Unauthorised query access | Token-based auth (24-char random, per session) | Token leak → data access. Mitigate: header-only auth (see recommendations) |
| Bulk exfiltration | KLS 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-middle | TLS everywhere (Cloudflare Tunnel = HTTPS, JinDesk = HTTPS) | Cloudflare is the TLS terminator — they could theoretically inspect traffic |
| JinDesk server compromise | JinDesk 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 compromise | DuckDB opened read-only. SQL whitelist blocks writes | Attacker with local access has bigger problems than the proxy |
| Tunnel URL guessable | Cloudflare Quick Tunnels use random subdomains (xxx.trycloudflare.com) | Not cryptographically random. For production: use named tunnels with authentication |
| Session token in URL | Currently supported via query param (?token=) | Leaks to browser history, logs, referrer headers. Mitigate: header-only auth |
| Stale proxy in directory | Deregistration on shutdown | Crash or network loss → stale entry. Mitigate: heartbeat protocol |
| CORS wildcard | Current: Access-Control-Allow-Origin: * | Any website knowing the token can query the proxy. Mitigate: restrict to JinDesk origin |
| SIS auto-attach | SIS 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):
-
Header-only authentication — remove query param token support (
?token=). Tokens must only travel inAuthorization: Bearerheaders. This prevents leakage to browser history, server logs, and referrer headers. -
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. -
Result size limits — add a configurable
--max-rowsflag (default: 10,000). Queries returning more rows are truncated with a warning. This limits the blast radius of bulk exfiltration. -
Explicit SIS opt-in — require
--with-sisflag to attach the SIS file. Default: KLS only. The data owner should consciously decide whether notebook data is exposed. -
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):
-
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.
-
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. -
Token rotation — allow
--token-ttlto auto-rotate the session token. When the token rotates, the proxy re-registers with the JinDesk. Active sessions get a new cookie transparently. -
IP allowlist —
--allow-ipflag to restrict which IPs can query the proxy (in addition to token auth). Defence in depth. -
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):
-
Bandwidth metering — track bytes served per session. Display in the proxy console and optionally enforce a cap.
-
Query complexity limits — reject queries with excessive JOINs, subqueries, or estimated cost (DuckDB’s EXPLAIN can estimate this).
-
Session expiry —
--session-ttl 4hto auto-shutdown the proxy after a time window. For scheduled review sessions.
The User Experience
Section titled “The User Experience”For the data owner (hospital IT, data engineer)
Section titled “For the data owner (hospital IT, data engineer)”# Build the KLS as usualjinflow make numetrix.rmc_west
# Share it — one commandjinflow-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 stopShare the “Open” URL with anyone who should have access. When you’re done, press Ctrl+C. The data disappears from the cloud instantly.
For the viewer (CFO, consultant, auditor)
Section titled “For the viewer (CFO, consultant, auditor)”- Receive a URL:
https://proxy.jinflow.io/s/k8m2np4qr7st9vwx - Click it — JinDesk opens with the tenant’s data
- Browse findings, hypotheses, entities — the full JinDesk experience
- Every query is answered in real-time from the owner’s machine
- 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.
Why This Is Better Than Cloud Storage
Section titled “Why This Is Better Than Cloud Storage”| Cloud (R2) | P2P2P | |
|---|---|---|
| Data location | Cloudflare R2 bucket | Owner’s machine |
| Data at rest | Encrypted (bucket policy) | Owner’s filesystem |
| Access control | IAM policies, bucket ACLs | Physical: proxy on/off |
| Revocation | Delete from bucket, purge caches | Ctrl+C |
| Revocation latency | Minutes (propagation) | Instant |
| Compliance audit | Cloud provider logs | Local query audit log |
| Availability | 24/7 (cloud SLA) | Only when proxy runs |
| Latency | ~50ms (edge cache) | ~200-500ms (tunnel round-trip) |
| Cost | Storage + egress | Zero (Cloudflare Quick Tunnels are free) |
| Setup | R2 credentials, sync profiles, bucket config | One 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.
Architectural Implications
Section titled “Architectural Implications”JinDesk becomes stateless
Section titled “JinDesk becomes stateless”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.
The KLS is the API
Section titled “The KLS is the API”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.
Multi-pack, multi-tenant, multi-machine
Section titled “Multi-pack, multi-tenant, multi-machine”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.
The Name
Section titled “The Name”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.
Implementation Roadmap
Section titled “Implementation Roadmap”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
Phase 3: Trust (enterprise features)
Section titled “Phase 3: Trust (enterprise features)”- Named Cloudflare Tunnels (stable URLs, access policies)
- Proxy identity (key pair signing)
- Token rotation (
--token-ttl) - IP allowlist (
--allow-ip) - Session expiry (
--session-ttl)
Phase 4: Federation (multi-JinDesk)
Section titled “Phase 4: Federation (multi-JinDesk)”- 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”
Authentication: Stealth Mode and Beyond
Section titled “Authentication: Stealth Mode and Beyond”P2P2P operates in a spectrum of trust:
Stealth Mode (unauthenticated)
Section titled “Stealth Mode (unauthenticated)”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?”
Identified Mode (GitHub auth)
Section titled “Identified Mode (GitHub auth)”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:
jinflow-proxy kls.duckdb --tunnel # stealth: anyone with URL can viewjinflow-proxy kls.duckdb --tunnel --require-auth # identified: GitHub sign-in requiredDefault is stealth. The data owner opts into identification when they need audit-grade sessions (e.g. regulatory reviews, consulting engagements).
One System, Multiple Operating Modes
Section titled “One System, Multiple Operating Modes”This is the same identity model across all deployment modes:
| Mode | Auth | Notebook | Audit |
|---|---|---|---|
Local (jinflow explore) | None (god mode) | Full access | Optional |
| Cloud (R2) | Demo read-only / GitHub auth | Auth required for writes | Yes |
| P2P2P stealth | None | Read-only | No |
| P2P2P identified | GitHub auth | Full access | Yes |
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 explorebinds 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.
The shape: loopback redirect
Section titled “The shape: loopback redirect”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_urlquery 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.ioapex 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.ioA → 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.tsshort-circuits whenJINFLOW_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.ioDNS 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-list | Configured to | Effect (envisioned) |
|---|---|---|
| Domains / Frontend Origins | <prod-domain> only | Clerk SDK on cloud JinDesk works |
| Redirect URLs / Paths | http://localhost + http://127.0.0.1 | Local launcher’s /auth-callback accepted |
Implementation surface (when this lands)
Section titled “Implementation surface (when this lands)”A future commit will realise this shape. The relevant surface:
| Path | Change |
|---|---|
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.svelte | Becomes 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.ts | clerkHandle 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.py | Sets 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 Dashboard | Once: 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.
Distinguishing the two flows in code
Section titled “Distinguishing the two flows in code”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 exploreon 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.
What stays valid from the section above
Section titled “What stays valid from the section above”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:
-
The
__sessioncookie holds a long-lived JWT, minted via a Clerk JWT template (similar pattern to the existingclitemplate 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 theclitemplate) 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. -
Signature verification happens offline.
@clerk/backend’sverifyToken({ 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 theclitemplate (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>/tokensto 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.
SIS in P2P2P: Where Does User State Live?
Section titled “SIS in P2P2P: Where Does User State Live?”The Problem
Section titled “The Problem”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:
- Privacy asymmetry — the KLS is hospital data (sensitive). The SIS is user activity (less sensitive, but still personal). They have different risk profiles.
- 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.
Option B: SIS on JinDesk (cloud-side)
Section titled “Option B: SIS on JinDesk (cloud-side)”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.
Option C: Hybrid (recommended)
Section titled “Option C: Hybrid (recommended)”- 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.
Offline Banner
Section titled “Offline Banner”When a proxy goes offline (tunnel drops, owner closes laptop, network interruption), JinDesk should communicate this clearly:
Detection
Section titled “Detection”JinDesk detects offline proxies via:
- Heartbeat timeout — no heartbeat for 3 minutes → mark stale
- Query failure — fetch to proxy URL fails → mark offline immediately
User Experience
Section titled “User Experience”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] │└──────────────────────────────────────────────────────────────────────┘Behavior
Section titled “Behavior”- 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.
How this manifests in jinflow
Section titled “How this manifests in jinflow”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.
The Elevator Pitch
Section titled “The Elevator Pitch”“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 →