AWS Marketplace·Enterprise deployment — listing in progress.Contact sales·View pricing

INTERTRACE — RUNTIME AI SECURITY • GATEWAY PROTECTION • RUNTIME VERIFICATION • BEHAVIORAL INTELLIGENCE • PROMPT INJECTION DEFENSE • PII REDACTION • SUB-50MS CLASSIFICATION • COMPLIANCE REPORTING • MANAGED AGENTS • OWASP LLM TOP 10 • INTERTRACE — RUNTIME AI SECURITY • GATEWAY PROTECTION • RUNTIME VERIFICATION • BEHAVIORAL INTELLIGENCE • PROMPT INJECTION DEFENSE • PII REDACTION • SUB-50MS CLASSIFICATION • COMPLIANCE REPORTING • MANAGED AGENTS • OWASP LLM TOP 10 • INTERTRACE — RUNTIME AI SECURITY • GATEWAY PROTECTION • RUNTIME VERIFICATION • BEHAVIORAL INTELLIGENCE • PROMPT INJECTION DEFENSE • PII REDACTION • SUB-50MS CLASSIFICATION • COMPLIANCE REPORTING • MANAGED AGENTS • OWASP LLM TOP 10 •
← Back

Clerk, Convex, and Supabase: three planes of trust

Essay · July 23, 2026 · 12 min readBy Samuel OyanEngineer
ClerkConvexSupabaseidentity

How Intertrace separates human identity, plan entitlements, and security telemetry—so RLS, billing flags, and sessions do not collapse into one brittle store.

A common architecture tweet says just use one backend. Intertrace uses three on purpose. Humans authenticate with Clerk on the platform host. Plan flags and commercial entitlements live in Convex. Gateway events, findings, assets, and org-scoped security data live in Supabase behind row-level security. Each system is good at one threat model. None of them should impersonate the others. The cost of the split is plumbing. The cost of not splitting is an audit in which your billing database is also your SIEM, or your identity vendor is asked to retain prompt payloads.

This is not a preference for more vendors. It is a refusal to let three questions share a failure domain: who is this human and which organization are they in; may this organization use this hub or limit; what did this organization’s AI traffic actually do. Mixing those questions produces the worst of all worlds: a findings table that cannot be queried under tenant policy, an identity system that starts looking like a security warehouse, or an entitlement flag that silently gates whether a detection is stored.

Session ≠ plan ≠ telemetry
Userbrowser sessionClerkidentity · orgsConvexplan flagsJWT bridgeSupabase RLSEntitlementsnot telemetrySupabaseevents · findings

The JWT bridge lets the dashboard read tenant rows. It does not mean Convex should store prompt payloads, or that Supabase should mint session cookies. Clerk answers who. Convex answers what the plan includes. Supabase answers what the traffic did.

What goes where

  • Clerk: sign-in, session cookies, human organization membership, SSO. Hosted for customers at platform.intertrace.ai. Sign-in is /sign-in. Mission Control uses the same Clerk application at mission.intertrace.ai/sign-in.
  • Convex: convex/entitlements.ts — whether a plan includes a hub, a limit, or an add-on. Plans are free, launch, scale, enterprise. Not findings. Not gateway_events. Not prompts.
  • Supabase: gateway_events, findings, finding_status_history, finding_evidence, assets, org_members, RLS keyed by org_id / contract tenant_id. This is the security telemetry store.

Runtime keys are a fourth credential, and they do not live in Clerk. Clients present Bearer itr_rt_* to the Fly data plane. ExtAuthz verifies them with POST /api/runtime/verify-key. Tenancy on that path is key-derived. A Clerk session proves a human may look at the dashboard. A runtime key proves an agent or SDK may speak to https://intertrace.fly.dev. Using one credential for both is convenient and wrong: humans revoke sessions, agents rotate keys, and incident response needs to disable a workload without locking the security lead out of Work.

Clerk is the human plane

Product identity is Clerk. Clerk manages user accounts, organization membership for people, and session cookies. The browser hits platform.intertrace.ai/sign-in. Clerk handles password, OAuth, or SSO. On success there is a __session cookie. Middleware authenticates that cookie through authenticateClerkRequest() and gets a Clerk userId, orgId, email, and name. That is enough to know which human is at the keyboard. It is not enough to query gateway_events under the RLS policies that were written against auth.uid() as a Supabase user UUID.

The bridge is a short-lived Supabase access JWT, minted from the Clerk session, signed with SUPABASE_JWT_SECRET, with sub equal to the backing Supabase auth.users id. PostgREST then sees auth.uid() as it always did. There is no separate Supabase Auth session and no magic-link hop. linkOrCreateSupabaseUserForClerk() finds or creates the auth.users row and upserts clerk_user_identities. Webhooks at POST /api/webhooks/clerk keep that link current when membership changes. Sign-out clears the Clerk session; minted tokens expire on their own TTL.

Hosts matter because muscle memory wants one origin. Marketing lives on intertrace.ai. Auth and the product dashboard live on platform.intertrace.ai. Mission Control lives on mission.intertrace.ai. Middleware 308s product paths off the marketing apex so a docs link cannot quietly become an unauthenticated dashboard render on the wrong host. Clerk redirect URLs, webhook URLs, and screenshot capture have to name platform, not the apex, not localhost, and not the Fly data plane.

What Clerk is not

Clerk is not where findings live. It is not where we store SecurityEventEnvelope payloads. It is not the RLS engine. Organization membership in Clerk is how a human arrives; org_members in Postgres is how the database decides whether a SELECT returns a row. If you only write to Clerk, the database cannot enforce tenant isolation on telemetry. If you only write to Postgres, the human cannot sign in. Both writes have to happen, and they have to agree.

Convex is the plan plane

Convex holds per-organization plan and feature flags. The server functions live in convex/entitlements.ts. Effective entitlements are plan defaults, plus add-ons, plus explicit overrides. The commercial catalog—Launch, Scale, Enterprise, and expansion packs—is kept aligned with lib/convex/plan-catalog.ts, addon catalogs, and the marketing pricing page. Client and server helpers in lib/convex/entitlements.ts (useEntitlements, getUserEntitlements) gate whether a hub or a limit is on.

That is a billing and packaging threat model. It answers: may this org use Graph, scheduled red team, SIEM export, SSO, custom policy packs. It does not answer what a model said, whether a tool call was authorized, or who resolved a finding. Those answers have retention, isolation, and evidence requirements that do not belong in a reactive app database optimized for subscription state.

Never store findings in Convex. Never store gateway_events there. Never store prompt previews, tool arguments, or classifier labels there. If a feature flag should hide a hub, hide the hub. Do not delete or relocate the telemetry. An org that downgrades from Scale to Launch still had traffic last month; the findings remain in Supabase under RLS, even if the UI no longer offers a given workflow. Entitlements gate product surface. They do not rewrite history.

  • free and launch exist so someone can sign in and see a real dashboard without implying that detection data was free to scatter across vendors.
  • scale and enterprise add runtime authorize, MCP gateway, tool guard, SIEM export, and similar flags. The data plane still writes to the same Postgres.
  • Legacy plan names (pro, business) still map in the catalog. Convex is allowed to be boring about aliases. It is not allowed to grow a shadow findings table to paper over a mapping.

Supabase is the telemetry plane

The dashboard API validates gateway traffic, writes gateway_events, may create findings, and updates posture. Unified events use SecurityEventEnvelope. Investigation UX reads org-scoped rows through RLS. This is the store that has to survive a review: events, findings, evidence, assets, graph tables, membership. Service-role keys exist for ingest and operator jobs. Member reads go through the minted JWT so auth.uid() maps into org_members.

Isolation is not a filter you remember to apply in application code. It is policies that key off org_id and membership. FORCE ROW LEVEL SECURITY is on for audit tables specifically so owner roles cannot shrug policies aside. Ingest uses the service role on purpose; that path is authenticated with INTERTRACE_INTERNAL_SECRET (or the legacy BASTION_INTERNAL_SECRET) between the Go enforcer / Fly edge and the Next.js API, not with a Clerk cookie. Mixing those authenticators is how a browser session starts minting runtime events, or a gateway starts reading other tenants’ findings.

Events are not sessions

A Clerk session is a human at a browser. A gateway event is a machine decision about a prompt, a tool, an MCP call, or an A2A message. The event carries tenant_id as a contract field; we persist org_id. The human looking at Traffic must be a member of that org. The agent that caused the event never had a Clerk session and should not. If you find a code path that uses a runtime key to read findings, or a session cookie to pass ExtAuthz, treat it as a defect even if the demo works.

Membership still has an SSOT in Postgres

org_members is the RLS and dashboard source of truth. Mission Control historically had organization_members. Writing to only one table is how you get I invited them but they cannot see findings, or they can see Mission Control and not Work. Triggers keep the two in sync. The migration is explicit: bidirectional sync with a transaction-local GUC (intertrace.membership_sync) so the triggers do not recurse, and a role mapper so Clerk-ish names like member and analyst land on viewer and security_analyst rather than inventing a third vocabulary.

Clerk org membership is how a human arrives. It does not replace those tables. Webhooks should result in a Postgres member row, not only a Clerk dashboard update. Invites that write only to organization_members leave RLS blind. Invites that write only to org_members leave Mission Control looking empty. The trigger is there because we found every onboarding path that inserted a row just this once.

  1. Human signs in at platform.intertrace.ai/sign-in (Clerk).
  2. Identity link ensures a Supabase auth.users row and clerk_user_identities.
  3. org_members grants RLS. organization_members mirrors for Mission Control.
  4. Convex entitlements decide which hubs render.
  5. Queries for gateway_events and findings run as the minted JWT, never as a guess at org_id from the client.

The JWT bridge is unglamorous and load-bearing

HS256 with a shared SUPABASE_JWT_SECRET is not fashionable. It is the smallest thing that makes existing RLS work without running a second login. The token is short-lived. It is minted server-side after Clerk has already authenticated the browser. It is not a runtime key. It is not an entitlement document. It does not carry plan flags; if a hub is gated, the Next.js app asks Convex, then still queries Supabase only for orgs the user belongs to.

Compromise models follow from that. Steal a Clerk session and you are that human, for that TTL, in that org, subject to RLS. Steal a Convex deploy key and you can lie about plans; you should not be able to read another tenant’s prompts. Steal the Supabase service role and you have the telemetry store; that key is the crown jewel of the control plane and is not distributed to browsers. Steal an itr_rt_* key and you can send traffic as that workload; you should not get a dashboard session. If any of those collapses into one token, the blast radius becomes the entire product.

Why not one database

We wanted one database. The threat models disagreed. Identity vendors are good at sessions, MFA, SAML, and org invites. They are a poor place to put a year of gateway_events with RLS that your own engineers must reason about in SQL. Reactive app backends are good at flags and presence. They are a poor place to put append-only finding_status_history with FOR UPDATE triggers and FORCE RLS. Postgres is good at that, and at the joins an investigation needs. It is a poor place to reinvent hosted sign-in and SSO.

There is also an organizational reason. Entitlement bugs should page a different owner than detection bugs. If a Scale flag flips off, Graph may hide; Traffic should still show last week’s blocks. If ingest is down, the sign-in page should still work and the plan should still read correctly, even though the Work hub is empty. Coupled stores turn every outage into a combined identity-billing-SIEM incident. That is dramatic and useless.

Failure modes of collapsing the planes

  • Findings in Convex: you will eventually want a SQL review, immutability triggers, and RLS that matches org_members. You will rebuild Supabase inside Convex, badly.
  • Sessions in Supabase Auth as a second product login: users will have two passwords or a magic-link sidecar. We already removed that hop.
  • Plan flags in Postgres that every request joins: you can do it, but you will ship pricing experiments through migrations, which is how catalog drift happens.
  • Org id from a client header on either the dashboard or the data plane: tenancy becomes an input. Production tenancy is membership for humans and verify-key for machines.
  • Storing prompts in Clerk user metadata: you have now given your identity vendor a copy of the thing the gateway was built to inspect, with a different retention story.

How a request actually crosses the planes

Consider a human opening a finding. The browser has a Clerk session on platform.intertrace.ai. Middleware validates it. The server mints a Supabase JWT for that linked user. The query hits findings under RLS: the user must appear in org_members for that org_id. The page may also call Convex to decide whether an upsell or a hub chrome should render. Convex returning false for a flag does not hide the finding from SQL; it hides a button. The finding’s status history and evidence stay in Postgres.

Consider an agent calling the data plane. There is no Clerk cookie. The Go enforcer’s ExtAuthz path calls POST /api/runtime/verify-key with the internal secret and the raw itr_rt_* key. On success, org_id is taken from runtime_api_keys, not from the caller. Inspect runs fail-closed. A SecurityEventEnvelope is emitted asynchronously toward dashboard ingest. Ingest writes gateway_events as the service role, still keyed by that org. No Convex write occurs. No Clerk user is created. If verify-key fails, there is no upstream model call and no event that pretends the traffic was in-policy.

Those two paths share an org_id and nothing else. That is the design. When they accidentally share a credential, or when a debug header supplies the org, you no longer have three planes. You have a demo.

Mission Control is the same identity, a different job

Mission Control at mission.intertrace.ai is not a second product identity. It uses the same Clerk application. It is an operator plane for people who need org-level command, observability, and support workflows. Its membership table is the mirror, not the source. If Mission Control could grant access that org_members does not, RLS on findings would be theater. If org_members could grant dashboard access that Clerk does not, humans would be in Postgres and unable to sign in. The trigger file is named like a chore because it is a chore, and because chores that are not automated become drift.

Operators still must not use the service role from a browser. Mission Control server routes can use elevated clients the same way ingest does, with the same obligation to never present another tenant’s rows to a principal who is not allowed to see them. The three-plane rule does not relax because the host is mission rather than platform.

What we tell ourselves when adding a table

  1. Is this a human session or org invite concern? Clerk, then a webhook into Postgres membership.
  2. Is this a plan, limit, or add-on? Convex entitlements, not a column on findings.
  3. Is this traffic, a finding, evidence, an asset, or a graph edge? Supabase, with org_id and RLS, and with audit helpers if status can change.
  4. Is this a machine credential? runtime_api_keys and verify-key, prefix itr_rt_*, never a Clerk JWT on the Fly edge.
  5. Would storing it in Convex make a future review impossible? Then it does not go in Convex.

The JWT bridge from Clerk into Supabase RLS is unglamorous plumbing. It is also the difference between a multi-tenant product and a shared spreadsheet with a login screen. Convex entitlements answer may this org use this hub. Supabase answers what did this org’s traffic do. Clerk answers who is this human, in which org. Collapsing those questions feels efficient until the first review in which someone asks you to prove isolation, prove retention, and prove that a downgrade did not erase detections. Keep the planes. Write the boring glue. Do not store findings in the billing plane.

Continue reading

← Back to blog