Skip to content

[in-progress] [cli] dev credentials for google#2591

Closed
stopachka wants to merge 5 commits into
mainfrom
sharedcred-cli
Closed

[in-progress] [cli] dev credentials for google#2591
stopachka wants to merge 5 commits into
mainfrom
sharedcred-cli

Conversation

@stopachka
Copy link
Copy Markdown
Contributor

@stopachka stopachka commented Apr 22, 2026

CLI counterpart to the dashboard changes in #2553. Stacked on top of sharedcred so the diff here is only the CLI.

What's new at a glance

Before After
add --type google always requires a real client_id (+ secret for web). add --type google --app-type web --dev-credentials creates a working client with zero setup.
No way to rotate or upgrade credentials — users delete + re-add, which churns the client's db id and breaks any in-flight refresh tokens. New update subcommand swaps credentials in place.
Interactive add for Google + web dumps the user straight into "paste your client_id". A credential-mode selector (same shape as the dashboard toggle) appears first, defaulted to dev.

Why now

The backend PR (#2588) and the dashboard PR (#2553) already landed these capabilities. Without the CLI, a developer who bootstraps their app with instant-cli init still has to open the dashboard just to create an OAuth client — defeating the "no setup" value prop of shared dev credentials. This PR closes that loop.

1. auth client add --dev-credentials

Interactive (non---yes) flow — what the user sees

$ instant-cli auth client add --type google

? Select a Google app type:
❯ Web  (Redirect Flows or Expo Auth Session)
  iOS
  Android
  Google Button for Web

? Credential mode:                       ← NEW
❯ Use dev credentials  — works on localhost / Expo with no Google setup
  Use my own           — provide your own client_id / client_secret

? Client Name:  google-web

⠋ Creating Google OAuth client...

┌──────────────────────────────────────────────────────────────┐
│ Google OAuth client created: google-web                      │
│ App type: web (dev credentials)                              │
│ ID: 5a7e8d9f-1234-4aaa-b000-c1d2e3f4a5b6                     │
│                                                              │
│ No setup required — works on localhost / Expo out of the box.│
│                                                              │
│ Ready for production? Run:                                   │
│   instant-cli auth client update --name google-web \         │
│     --client-id <id> --client-secret <secret>                │
└──────────────────────────────────────────────────────────────┘

The credential-mode selector is deliberately not a Yes/No. Mirroring the dashboard's two-option toggle (Use dev credentials / Use my own) makes the alternative discoverable — users who don't know shared creds exist still learn about them when they run add for the first time.

When the selector is skipped

The selector exists to resolve ambiguity. When the user's flags already commit to a mode, we skip it:

User's flags Selector shows? Mode
--dev-credentials no dev
--client-id <x> (or --client-secret) no custom
none of the above, --yes no custom (fall through to existing required-flag errors)
none of the above, interactive yes user picks
--app-typeweb no custom (shared creds are web-only)

Validation

Three new error paths, each with a specific message:

$ instant-cli auth client add --yes --type google --app-type web \
    --dev-credentials --client-id X
[error] --dev-credentials cannot be combined with --client-id,
        --client-secret, or --custom-redirect-uri. Omit them
        (Instant supplies the credentials) or drop --dev-credentials.

$ instant-cli auth client add --yes --type google --app-type ios \
    --dev-credentials
[error] --dev-credentials is only supported for --app-type web.
        Native clients need a real client_id from Google.

$ instant-cli auth client add --yes --type github --dev-credentials
[error] --dev-credentials is only supported for --type google at the
        moment.

Request payload (shared-creds path vs existing web path)

  POST /dash/apps/:app_id/oauth_clients
  {
    provider_id: "prov-...",
    client_name: "google-web",
-   client_id: "123-abc.apps.googleusercontent.com",
-   client_secret: "GOCSPX-...",
    authorization_endpoint: "https://accounts.google.com/o/oauth2/v2/auth",
    token_endpoint:         "https://oauth2.googleapis.com/token",
    discovery_endpoint:     "https://accounts.google.com/.well-known/openid-configuration",
-   redirect_to: "https://api.instantdb.com/runtime/oauth/callback",
    meta: {
      appType: "web",
      skipNonceChecks: true,
+     useSharedCredentials: true,
    }
  }

Endpoints stay the same on purpose — the record shape should be uniform whether or not it uses shared creds. The server substitutes client_id / client_secret at request time, triggered by meta.useSharedCredentials.

2. auth client update (new subcommand)

What it does

One subcommand covers two flows, mirroring the dashboard's unified CredentialsEditor:

  • Rotate — existing custom-cred client → new credentials.
  • Upgrade — shared-cred client (created with --dev-credentials) → its own credentials; meta.useSharedCredentials flips from true to false.

The implementation is just one POST — update-oauth-client on the server (shipped in #2588) accepts either or both of client_id / client_secret along with a meta merge. The CLI always sends meta: { useSharedCredentials: false }, which is idempotent on already-custom clients and performs the upgrade on shared-cred ones.

Interactive flow

$ instant-cli auth client update

? Select a client to update:
  google-web     |   c-web-a1b2
❯ g-dev           (dev credentials)   |   c-dev-7f3a
  google-ios     |   c-ios-55ea

? Client ID:
❯ 123-abc.apps.googleusercontent.com

? Client Secret:
❯ ••••••••••••••••••

⠋ Updating credentials...

┌──────────────────────────────────────────────────────────────┐
│ Credentials updated for g-dev.                               │
│                                                              │
│ If this client was using dev credentials, it is now using    │
│ the client_id / client_secret you provided.                  │
└──────────────────────────────────────────────────────────────┘

The (dev credentials) suffix in the picker comes from meta.useSharedCredentials on each client — critical signal for telling which clients are currently on shared creds.

Prompt decision tree

                 ┌────────────────────────────────────┐
                 │ Resolve client                     │
                 │ (--id | --name | picker)           │
                 └────────────────────────────────────┘
                                   │
                                   ▼
                 ┌────────────────────────────────────┐
                 │ Read client.meta.appType           │
                 └────────────────────────────────────┘
                                   │
             ┌─────────────────────┴─────────────────────┐
             ▼                                           ▼
   appType === 'web' or absent            appType ∈ {ios, android, button-for-web}
             │                                           │
             │                                           ▼
             │                                 secret isn't prompted
             │                                 (native clients don't have one)
             ▼
   Both client_id + client_secret
   are candidates to prompt for.
                                   │
                                   ▼
            ┌────────────────────────────────────────────┐
            │ Did the user pass --client-id or           │
            │ --client-secret on the command line?       │
            └────────────────────────────────────────────┘
                                   │
                ┌──────────────────┴───────────────────┐
                ▼                                      ▼
          yes, one or both                         no (interactive only)
                │                                      │
                │                                      ▼
                ▼                             Prompt for each field that
        Skip all prompts; send                applies to this appType.
        only the flags that were
        explicitly provided.

The partial-update shape ("supplying only one field is fine") matches how the API works — both fields are optional on the server — and matches what users actually want when rotating just the secret.

--yes mode

Two errors guard --yes:

$ instant-cli auth client update --yes --client-id X --client-secret Y
[error] Must specify --id or --name.

$ instant-cli auth client update --yes --name g-dev
[error] Must specify at least one of --client-id or --client-secret.

Changes at file level

  • client/packages/cli/src/commands/auth/client/add.ts
    • New selectCredentialMode effect — UI.Select that resolves to 'dev' | 'custom'.
    • handleGoogleClient branches on shared-creds mode: skips id/secret/redirect prompts, sends the shared-creds payload, prints the new success summary.
    • Validation guards for the three error paths listed above.
    • Non-Google validation lives in authClientAddCmd before dispatch.
  • client/packages/cli/src/commands/auth/client/update.ts (new)
    • authClientUpdateCmd effect.
    • resolveClient helper that handles --id / --name / picker + the (dev credentials) suffix rendering.
    • promptClientId / promptClientSecret effects (had to inline rather than use optOrPrompt — its skipIf helper errors when a flag value is present, which is wrong for partial updates where supplying a flag should mean "don't prompt for this one but do still accept it").
  • client/packages/cli/src/lib/oauth.ts
    • updateOAuthClient({ appId?, oauthClientId, body }) — thin POST wrapper. body is pass-through so the helper doesn't get opinionated about the update shape.
  • client/packages/cli/src/index.ts
    • --dev-credentials flag + help-text entry on authClientAddDef.
    • New authClientUpdateDef command def mirroring authClientDeleteDef for identification + adding credential flags.
  • client/packages/cli/__tests__/authClientAddGoogle.test.ts
    • Updated the "missing --client-id → prompts for id" test: the new credential-mode selector lands before the id prompt, so the test now asserts on prompts[0] (selector) and prompts[1] (id) explicitly.
    • 4 new cases: --yes shared-creds success, --dev-credentials + --client-id error, --dev-credentials + --app-type ios error, interactive "pick dev creds" success.
  • client/packages/cli/__tests__/authClientUpdate.test.ts (new)
    • 10 cases covering --yes happy/error paths, interactive picker, partial updates, and native-client secret-prompt skipping.

Design decisions

  • Flag name --dev-credentials rather than --shared-credentials. Matches the dashboard copy ("Use dev credentials") which itself was chosen because "dev" is immediately meaningful to the user — they know what they're opting into.
  • Default in the interactive selector is dev, not custom. Mirrors the dashboard. The zero-setup path is the path most users probably want on first contact; anyone who knows they want their own creds will skip past the selector anyway (either via --client-id on the command line or by picking option 2).
  • update always sets meta.useSharedCredentials: false, regardless of the client's current state. The operation is semantically "I'm providing my own credentials now." If we ever need the inverse (go back to dev creds after setting custom ones), that would be an explicit separate flag.
  • update v1 scope is just --client-id / --client-secret, not --custom-redirect-uri / --name / other meta fields. Keeps the diff small and matches what the dashboard's "Update credentials" button does today. Can extend later without API changes.
  • Partial updates allowed interactively too. If the user passes just --client-id, we don't nag for --client-secret — they told us what they wanted.

Pre-existing bug worth a follow-up (not fixed here)

The CLI hardcodes skipNonceChecks: true for all Google clients it creates (add.ts). The dashboard uses isNative(appType)true only for iOS/Android, false for Web / Button-for-Web. Web clients shouldn't be opting out of nonce checks. Worth a one-line fix in a follow-up.

Test plan

  • pnpm --filter instant-cli exec vitest run — 102 unit tests pass across 8 files. 30 e2e failures are unrelated (same failure set on main; they require infrastructure not available locally).
  • pnpm exec tsc -p packages/cli/tsconfig.build.json --noEmit — clean (ignoring the pre-existing minimist type error on main).
  • Manual against local dev server:
    • instant-cli auth client add --type google --app-type web --dev-credentials --name g-dev → shared-cred client created; sign in on http://localhost:3000 succeeds.
    • instant-cli auth client add --type google --app-type web → interactive selector appears, dev is default.
    • instant-cli auth client update --name g-dev --client-id X --client-secret Y → credentials rotated, same db id, meta.useSharedCredentials clears to false.
    • instant-cli auth client update → picker lists clients; g-dev shows (dev credentials) suffix.
    • Re-run update on the now-custom client → rotates, no meta change (idempotent).

Backend foundation for shared OAuth credentials. Splits the server-
only changes out of the full sharedcred PR so it can be reviewed on
its own; the dashboard UI follows in a separate PR rebased on top.

## What this enables

Apps can opt an OAuth client into Instant-provided dev credentials
instead of supplying their own client_id / client_secret. Primary
use case: a developer can test Google sign-in on localhost / exp://
without ever touching Google Cloud Console.

## Config

- New flag `shared-oauth-clients` in instant-config (keyed by
  provider name, contains clientId + encryptedClientSecretHexString).
  See `server/flags-config/instant.schema.ts` + `server/src/instant/
  flags.clj`.

## Model changes

server/src/instant/model/app_oauth_client.clj
- `use-shared-credentials?` predicate on a client's meta.
- `get-shared-credential!` (throws when absent) and
  `get-provider-by-id!` (canonical provider-row lookup; returns the
  full record so callers destructure).
- `->OAuthClient` now derives provider-name from provider_id and, on
  a shared-credential client, substitutes the app's row with the
  shared cred values at request time. No more meta.providerName.
- `assert-shared-credentials-allowed!` enforces the 100-user cap
  (development-only guardrail). Callers: oauth-clients-post,
  update-oauth-client, and the new-user branch of
  upsert-oauth-link!.
- `update!` extended to accept :client-id and :encrypted-client-
  secret so the upgrade-from-shared flow is an in-place update
  instead of delete + recreate.

server/src/instant/model/app_authorized_redirect_origin.clj
- `shared-credentials-default-match` auto-allows localhost
  (http/https) and exp:// for shared-credential clients, without
  them having to register those origins manually. Custom domains
  still go through the existing authorized-origins list.

server/src/instant/model/app_user.clj
- `users-at-least?` bounded existence check: SELECT COUNT(*) over a
  LIMIT-n subquery of the $users.id attr. Used by the cap above.

## Route changes

server/src/instant/runtime/routes.clj
- Origin/redirect-uri validation consolidated behind
  `assert-authorized-redirect-uri!` /
  `assert-authorized-request-origin!`. Both branch on
  use-shared-credentials? to fall back to the default-match.
- `oauth-id-token-callback` now validates origins too (previously
  gated on client_secret, which shared-cred rows don't have on the
  raw record).
- 100-user cap checked inside upsert-oauth-link!'s new-user branch
  so returning users are never blocked.

server/src/instant/dash/routes.clj
- oauth-clients-post / update-oauth-client: when
  meta.useSharedCredentials is true, require that a shared cred
  exists for the provider (via get-shared-credential!) and
  enforce the user cap before saving.
- update-oauth-client now accepts client_id / client_secret so the
  dashboard's in-place "update credentials" action can swap creds
  without destroying and recreating the client.
Dashboard UI on top of the server-side foundation in
sharedcred-backend. Users can now pick "Use dev credentials" when
adding a Google OAuth client and sign in on localhost / Expo with
no Google Cloud Console setup. When they're ready for production,
they can swap in their own client_id / client_secret in place.

## AddClientForm (Google)

- Toggle between "Use dev credentials" and "Use my own" (web clients
  only; native clients still require a real client_id).
- Shared-creds mode omits the client_id / client_secret inputs and
  shows a brief note explaining what's happening.
- GitHub: drop `meta: { providerName: 'github' }` from the create
  call (server derives provider-name from provider_id now).

## GoogleClient (per-client panel)

- New `CredentialsEditor` subcomponent that owns the credentials
  section and encapsulates the upgrade / rotate flow:
  - Shared-credentials clients see the "Using Instant's dev
    credentials" notice with "Set custom credentials" to opt out.
  - Custom-credentials clients see the existing client_id Copyable
    with an "Update credentials" button for rotating in place.
  - Both share the same edit form (inputs + Save / Cancel) wrapped
    in a `<form>` with Enter-to-submit + password-manager opt-outs.
- Update uses the new in-place `updateClient` endpoint added in the
  backend PR, so credential rotation no longer destroys and
  recreates the client.
- `GoogleClient` renamed from `Client` to match the other provider
  panels' naming.

## shared.tsx

- Generic `updateClient` helper: `{ token, appId, oauthClientID,
  body }`. Body passes through unchanged to `POST
  /dash/apps/:app_id/oauth_clients/:id`.

## MainDashLayout

- Pin the dashboard wrapper with `fixed inset-0` on the outer
  column. Decouples the dashboard from html/body height cascade so
  the "body shows through below the app" seam stops recurring (the
  same line had been bounced between h-full / min-h-full /
  h-[100dvh] three times; each trade-off was a symptom of the same
  underlying coupling).

## Copytext

- Lighter background in light mode so the inline URL badges don't
  read as dark pills on the grey dashboard panel.
Mirrors the dashboard's "Use dev credentials" toggle for `instant-cli
auth client add`. With `--dev-credentials` set (or picked in the
interactive credential-mode selector) on --type google --app-type web,
the CLI creates an OAuth client with meta.useSharedCredentials: true
and omits client_id / client_secret / redirect_to. The server
substitutes Instant-provided credentials at request time, so the
client works on localhost / Expo with no Google Cloud Console setup.

Interactive flow for Google web adds a new step between app type and
name: a two-option selector (Use dev credentials / Use my own)
defaulted to dev — same default the dashboard uses. Skipped when
--client-id or --dev-credentials is already supplied.

Validation:
- --dev-credentials cannot be combined with --client-id, --client-
  secret, or --custom-redirect-uri.
- --dev-credentials is only supported for --app-type web.
- --dev-credentials is only supported for --type google at the moment.

Success summary for the shared-creds path replaces the "Add this
redirect URI in Google Console" block with a "No setup required" note
and a hint pointing at `auth client update` for the production
upgrade path.
New `instant-cli auth client update` subcommand. Wraps the server's
in-place update route at POST /dash/apps/:app_id/oauth_clients/:id, so
users can rotate a client_id / client_secret pair without destroying
and recreating the client (which churned the database id and broke
in-flight sessions).

Covers two flows with one command, mirroring the dashboard's unified
CredentialsEditor:
- Rotate: an existing custom-cred client gets new credentials.
- Upgrade: a shared-cred client (created with --dev-credentials) gets
  its own client_id / client_secret, transitioning meta.
  useSharedCredentials from true to false.

Flags:
- --id <id>             identify by database id (exclusive with --name)
- --name <name>         identify by client name
- --client-id <val>     new client_id
- --client-secret <val> new client_secret (prompt masked)
- -a --app <app-id>     app; same default as the other subcommands

Interactive behaviour:
- No --id / --name → UI.Select picker listing clients. Shared-cred
  clients show a "(dev credentials)" suffix so the user can tell them
  apart.
- No field flags → prompt for client_id and (for web clients)
  client_secret. Native Google clients (meta.appType === 'ios' /
  'android' / 'button-for-web') skip the secret prompt since they
  don't have one.
- Supplying one field explicitly skips the other prompt — partial
  updates are fine.

--yes mode:
- Requires --id or --name (error matches `delete`).
- Requires at least one of --client-id / --client-secret.

The update body always sends meta: { useSharedCredentials: false }.
Idempotent on already-custom clients; flips shared-cred clients over.
This matches the dashboard's "Update credentials" / "Set custom
credentials" behaviour — the operation is "I'm providing my own
credentials now."
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 98207218-fba8-4298-abdb-44df0d411fab

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sharedcred-cli

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@stopachka stopachka changed the title CLI: --dev-credentials on auth client add + new auth client update [in-progress] [cli] dev credentials for google Apr 22, 2026
@github-actions
Copy link
Copy Markdown
Contributor

View Vercel preview at instant-www-js-sharedcred-cli-jsv.vercel.app.

@stopachka stopachka force-pushed the sharedcred branch 4 times, most recently from ea6cd20 to 6ca4780 Compare April 24, 2026 19:58
Base automatically changed from sharedcred to main April 29, 2026 23:59
@stopachka stopachka closed this Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant