Skip to content

fix: secure the Convex data plane + rebuild the auth template - #2

Open
ryanpacker wants to merge 4 commits into
mainfrom
fix/convex-data-plane-secure-defaults
Open

fix: secure the Convex data plane + rebuild the auth template#2
ryanpacker wants to merge 4 commits into
mainfrom
fix/convex-data-plane-secure-defaults

Conversation

@ryanpacker

@ryanpacker ryanpacker commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Two phases of the Convex data-plane auth work. Full architecture, research corrections, and the one remaining open item are in docs/convex-data-plane-auth.md.

Phase 1 — secure the data plane by default

  • _generated/server.js stub exposure (both templates): the committed stub aliased internalQuery/internalMutation/internalAction to the public generics, registering internal functions on the public API. Convex CLI ≥1.39.0 skips rewriting server.js when it exists, so the wrong stub ships to real deployments. In the auth template this exposed auditLog.record (anonymous forged-audit write) and auditLog.listAll (anonymous full-table read). Fixed with genuine codegen output; proven live this session (internal fn was publicly callable before, "Could not find public function" after).
  • standard data-plane lie: the messages demo was world-read/write behind a login page implying protection. Relabeled everywhere as an explicit public guestbook.
  • Regression guard: tests/guard-generated-server.mjs (npm test + CI workflow).
  • Cleanup: dropped leaked ./template.json/./template-info.json from standard's generated output; removed the orphan auth/starter.json.

Phase 2 — make the auth template actually authenticate + enforce it

As shipped, the auth template authenticated no requests and didn't even generate a building project. Rebuilt against Convex's official reference:

  • Correctness: auth.config.ts OIDC → dual customJwt (the OIDC form can't work with WorkOS); the client bridge used isLoaded (SDK field is loading) so Convex never authenticated, and ignored forceRefreshToken — rewritten to the canonical hook; start.ts createStartHandlercreateStart.
  • Enforcement: new convex/functions.ts (authedQuery/authedMutation/authedAction + explicit publicQuery/publicMutation + requirePermission), all modules migrated, and an ESLint ban on the raw builders (verified it fires).
  • Authz fixes: users.getByWorkosId (public, took a client workosId) → internalQuery; auditLogReader.listRecent documented as demo-only with a production gating note.
  • Setup that was missing: convex.json zero-config authKit block; README now documents the mandatory dashboard-only WorkOS JWT-template aud step and setting WORKOS_* on the Convex deployment. SDK → ^0.11.0 (0.8.2–0.9.x carry an IDOR), added @workos-inc/node/convex-helpers, convex ^1.42.0, Node ≥22.11, created the missing template-info.json.

Template/base drift (both templates)

Generated projects failed to build under the current @tanstack/cli base: vite-tsconfig-paths missing from packageAdditions, deprecated baseUrl in tsconfig, removed devtools triggerImage prop; auth's template.json also lacked the required deletedFiles. Fixed in both.

Verified

Both templates generate via @tanstack/cli create. Generated auth project typechecks + builds clean; generated standard builds clean (its 5 tsc errors are pre-existing iron-session legacy). ESLint ban fires on a raw-builder import. npm test guard passes.

NOT in this PR (needs you)

The one unproven item: a real signed-in token resolving to a non-null getUserIdentity() live. Blocked on (1) npx convex login on your personal Convex team (this machine's token is the BambooHR account) and (2) the dashboard-only WorkOS JWT-template aud step. Negative/security cases are deterministic from code and the mechanism was proven live in Phase 1.

🤖 Generated with Claude Code

ryanpacker and others added 3 commits July 8, 2026 13:27
Phase 1 of the Convex data-plane auth work (see
docs/convex-data-plane-auth.md). Closes two real exposures in the
generated-project templates and adds a regression guard.

_generated/server.js stub exposure (both templates)
  The committed convex/_generated/server.js aliased internalQuery/
  internalMutation/internalAction to the PUBLIC generics, so internal
  functions registered on the public API. Convex CLI >= 1.39.0 skips
  rewriting server.js when it exists, so the wrong stub is what gets
  pushed on convex dev/deploy -- shipping to real deployments, not just
  fresh scaffolds. In the auth template this exposed auditLog.record
  (anonymous forged-audit write) and auditLog.listAll (anonymous
  full-table read). Replaced both with genuine codegen output mapping
  the internal builders to the internal generics. Verified live this
  session: an internal function was publicly callable with the buggy
  stub and "Could not find public function" after the fix.

standard template data-plane lie
  The messages demo used raw query/mutation with no auth, so the table
  was world-read/write despite living behind the app login wall.
  Relabeled everywhere (function headers, schema, demo-page banner,
  R9STACK.md) as an intentionally public, unauthenticated guestbook, and
  pointed to the auth template for the enforced per-user pattern. Bumped
  convex ^1.31.2 -> ^1.42.0.

Regression guard
  tests/guard-generated-server.mjs (wired to npm test and a new CI
  workflow) fails if any template's committed server.js regresses the
  internal aliasing.

Also: re-synced standard/template.json from source and dropped the
leaked ./template.json / ./template-info.json file entries (a
starter->template rename left the compiler ignoring starter* but not
template*, so authoring files -- including a stale self-copy of the old
buggy stub -- were written into every generated project); removed the
orphan auth/starter.json legacy artifact. Versions: standard 1.2.0,
auth 1.0.1.

The auth template still does not authenticate requests as shipped
(OIDC config, isLoaded/loading bridge bug, getByWorkosId IDOR); that
rebuild is Phase 2 and needs a live-deployment test before promotion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…force it

Phase 2. The auth template did not authenticate a single request as shipped,
and did not even generate a building project. This rebuilds it against Convex's
official TanStack-Start + WorkOS AuthKit reference, then adds enforcement.

Correctness (it now authenticates)
- auth.config.ts: replace the OIDC { domain, applicationID: 'convex' } providers
  (which can never work — WorkOS serves no OIDC discovery doc and issues no
  aud='convex') with the dual customJwt providers Convex actually validates.
  Throw on missing WORKOS_CLIENT_ID instead of a placeholder that silently
  builds a bogus config.
- src/lib/convex-auth.ts: the bridge destructured `isLoaded` (the SDK field is
  `loading`), so Convex's "auth ready" signal was permanently wrong and it never
  authenticated; and it ignored `forceRefreshToken`. Rewritten to the canonical
  { loading, user } + getAccessToken/refresh hook.
- src/start.ts: old createStartHandler API -> createStart (current TanStack).

Security
- convex/functions.ts (new): authedQuery/authedMutation/authedAction inject the
  verified identity; publicQuery/publicMutation are explicit opt-outs;
  requirePermission for RBAC. All function modules migrated onto them.
- ESLint bans the raw query/mutation/action builders (and the convex/server
  generics), so no function can ship without an auth decision; functions.ts is
  the sole exemption.
- users.getByWorkosId was a public query taking a caller-supplied workosId
  (any authed user could read any user by id) -> now internalQuery.
- auditLogReader.listRecent documented as demo-only with a production gating note.

Setup that was missing / wrong
- convex.json (new): Convex zero-config authKit block (dev/preview/prod).
- README: the mandatory WorkOS Sessions JWT-template `aud` + profile-claims step
  (dashboard-only; auth silently fails without it) and setting WORKOS_* on the
  Convex deployment.
- Deps: @workos/authkit-tanstack-react-start ^0.11.0 (0.8.2–0.9.x carry an IDOR),
  add @workos-inc/node ^10.7.0 + convex-helpers ^0.1.120, convex ^1.42.0,
  Node >=22.11. Added the missing template-info.json.

Template packaging / base drift (both templates)
- Generated projects failed to build under the current @tanstack/cli base:
  vite-tsconfig-paths was missing from packageAdditions; tsconfig `baseUrl` is
  deprecated under TS6; the devtools `triggerImage` prop was removed. Fixed in
  both standard and auth. Auth's template.json also lacked the required
  `deletedFiles` field (so it never generated).

Verified: both templates generate via `@tanstack/cli create`; the generated auth
project typechecks and builds clean; the generated standard project builds clean
(its 5 tsc errors are pre-existing iron-session legacy). The ESLint ban fires on
a raw-builder import. NOT verified live: a real signed-in token resolving to a
non-null getUserIdentity() — that needs a Convex login on the personal team plus
the one dashboard-only WorkOS JWT-template step. See docs/convex-data-plane-auth.md.

Versions: auth 1.0.1 -> 2.0.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…en live check

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ryanpacker ryanpacker changed the title fix(templates): secure the Convex data plane by default fix: secure the Convex data plane + rebuild the auth template Jul 8, 2026
…pen hotkey

The TanStack devtools default hotkey (Shift+A) fires while typing capital A;
we never open the panel by keyboard, so drop the hotkey entirely and keep the
trigger hidden until hovered. Config only seeds first-run state -- existing
browsers with persisted tanstack_devtools_settings in localStorage are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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