JJETSYNGet in touch

Authentication

How sign-in and access work across Jetsyn's surfaces: the web app session cookie (email magic link or Google), the CLI device-authorization grant with a locally stored token, and the server-side entitlement gate that resolves every request to your one cell.

Jetsyn has one account behind two surfaces. You sign in to the web app with an email link or Google, and you sign in to the CLI once from your browser. Either way you land in the same, single cell, and every request is authorized on the server before it touches your data.

One account, two surfaces, one cell#

Whether you come in from the browser or the terminal, Jetsyn resolves your email to exactly one Jetsyn Cloud cell. The web app and the CLI are two doors into that one cell, never two accounts and never two copies of your data. Sign in on either and you are working the identical files.

Authentication is split cleanly by surface. The web app carries a browser session cookie. The CLI runs a device-authorization grant: you approve the terminal once from a browser, and an opaque token lives on your machine after that. Both resolve to the same account and the same cell.

Available now

Two sign-in methods in the web app, email magic link and Google, plus one browser-approved device sign-in for the CLI. All three map to the same Jetsyn account and the same cell.

Web app sign-in#

The web app signs you in two ways, and both end at the same session. There is no password to store.

Email magic link. You enter your email and Jetsyn sends a one-time link. POST /api/auth/request with { email } mints a single-use, signed, time-boxed login token and emails you a link back to the app. Clicking it hits GET /api/auth/verify?token=..., which verifies the token (single-use, signature-checked, expiring), starts your session, and redirects you into the product.

Google. Sign in with Google sends you through GET /api/auth/google/start to Google's consent screen (OAuth 2.0 authorization-code flow) and back to GET /api/auth/google/callback. Jetsyn exchanges the code server-side, requires a verified email, and mints the exact same session the email link does.

EndpointWhat it does
POST /api/auth/requestTake { email }, mint a single-use signed login token, email the magic link.
GET /api/auth/verify?token=...Verify the link, set the jetsyn_session cookie, redirect into the app.
GET /api/auth/google/startHand off to Google's consent screen (authorization-code flow).
GET /api/auth/google/callbackExchange and verify server-side, set the same jetsyn_session cookie.

Your web session is a stateless, signed token in a jetsyn_session cookie. There is no server-side session table to look up. The cookie itself carries the signed claim, and the backend verifies the signature on every request. Magic link and Google mint the identical cookie, so the two methods are interchangeable.

  • HttpOnly, so page scripts can never read it.
  • Secure, so it only travels over HTTPS.
  • SameSite=Lax, so an email or OAuth redirect still carries it on the first navigation.
  • A 7-day lifetime, after which you sign in again.
  • Scoped to the Jetsyn web domain, and separate from the token your CLI holds, so a browser cookie can never reach a tenant cell directly.

CLI sign-in (device authorization)#

A browser session cookie must not live inside a long-running terminal, so the CLI does not reuse it. Instead it runs an OAuth 2.0 device-authorization grant: you approve the terminal once in a browser, and the CLI receives its own long-lived token to store locally. One command starts it.

bash
jetsyn login      # or just `jetsyn`, which signs you in on first connect
  1. 1

    Start the grant

    The CLI calls POST /api/cli/device-start and gets back a CLI-held secret (device_code), a short human code (user_code), a verification_uri, a poll interval, and an expiry. Nothing is signed in yet.

  2. 2

    Approve in the browser

    Your browser opens to the Jetsyn sign-in page with the code prefilled, so there is nothing to paste. Sign in with your email link or Google if you are not already, then approve the terminal. The page binds the code to your account via POST /api/cli/device-claim, authorized by your web session.

  3. 3

    The CLI collects its token

    While you approve, the CLI polls POST /api/cli/device-token with its device_code. It sees { "status": "pending" } until you finish, then receives a long-lived, opaque access_token and your cell's workspace_slug, returned exactly once.

  4. 4

    Token stored locally

    The CLI writes the token to ~/.jetsyn/credentials with 600 permissions. The server keeps only a SHA-256 hash of it at rest, never the plaintext, and the CLI passes it through an environment variable, never on the command line.

  5. 5

    Connected

    Every later run presents the token as an Authorization: Bearer header and drops you straight into your cell with no re-prompt. jetsyn logout deletes the local token.

EndpointPurposeAuth
POST /api/cli/device-startBegin the grant; returns device_code, user_code, verification_uri, interval, expires_in.None
POST /api/cli/device-claimBrowser binds the user_code to your signed-in account.Web session
POST /api/cli/device-tokenCLI polls; pending until approved, then returns access_token + workspace_slug.device_code
GET /api/cli/meConfirm the token; returns email, super_admin, workspaces.Bearer token
~/.jetsyn/credentials (permissions 600)
token=<opaque token, presented as a Bearer>
email=you@company.com
workspace=your-cell

Tip

The device grant is invite-friendly and cross-device: if the browser does not open, or you approve on another machine, visit the verification_uri and type the short user_code shown in your terminal.

One account, one cell#

The first time you sign in on the CLI, Jetsyn ensures you own a cell and hands its slug back in the token response. The web app resolves the same cell for the same account. It is one resolver keyed on your identity, so your terminal and your browser open the identical cell, never a second store.

text
# CLI, right after you approve in the browser
POST /api/cli/device-token   { "device_code": "..." }
-> { "access_token": "<opaque token>", "workspace_slug": "your-cell" }

# Confirm the token and read your identity
GET /api/cli/me   (Authorization: Bearer <token>)
-> { "email": "you@company.com",
     "super_admin": false,
     "workspaces": [ { "slug": "your-cell" } ] }

Same cell, either surface

A file you edit from the CLI shows up in the web app immediately, because both surfaces read the same cell. Sign-in only decides how you reach that cell, never which cell you get.

How access is gated#

Signing in proves who you are. A second, separate check decides what you may do. Entitlement is resolved on the server for every gated call, and never trusted from the client.

The backend is the single authority. GET /api/me returns your identity and your entitlement together, so a frontend or an integration reads one place to know what to allow.

  • email, user_id, role, super_admin identify you.
  • workspaces lists the cell slugs you can reach.
  • plan and status are your subscription truth; entitled is the derived unlock.
  • onboarding_completed tells the app whether to route you into setup or straight to work.

The unlock rule is binary: you are entitled when your subscription status is active or trialing, and a super-admin is always allowed. A gated action you are not entitled to run returns HTTP 402 with { "error": "billing_required" }, the app's signal to open checkout. See Surfaces & tiers for what each plan unlocks over the same cell.

The CLI is gated the same way, one layer deeper: a cell only opens for a member of that cell (or a super-admin), and the membership check fails closed on any uncertainty. Your token proves identity; the cell mount re-checks that this identity may open this slug on every connect.

Never gate on the client

A client-side plan or tier value is for display only. Always re-check entitlement server-side (via /api/me or the backend gate) before running a paid action. A new route that skips the check can silently ship ungated.


Where to go next#