Skip to content

docs(oddkit-sales): unified account launch plan + MCP bearer-token handoff#214

Merged
klappy merged 1 commit into
mainfrom
unified-account-launch-plan
May 16, 2026
Merged

docs(oddkit-sales): unified account launch plan + MCP bearer-token handoff#214
klappy merged 1 commit into
mainfrom
unified-account-launch-plan

Conversation

@klappy
Copy link
Copy Markdown
Owner

@klappy klappy commented May 16, 2026

What this lands

Two canon artifacts from a planning session that resolved the customer-surface build:

  1. docs/oddkit/sales/unified-account-launch-plan.md — the Lovable build spec. One session ships every tier at once (Personal, Pro, Team, Team Pro paid; Mission + FA application-gated; Not-sure as fallback). Supabase + Lovable + Stripe substrate. Includes:

    • Full data model with RLS sketches (profiles, tier_interests, fa_applications, mission_applications, pats, anthropic_keys, referrals, credit_ledger)
    • FA state machine (10-slot cap, case-study contract, $10K MRR trapdoor)
    • Referral system (free-month coupons + bonus-token ledger deferred to metering)
    • BYO Anthropic key encrypted dormant via Supabase Vault
    • ToS + Early Access Agreement + Privacy required at payment surface
    • 6B Table A satisfying klappy://canon/constraints/borrow-evaluation-before-implementation
    • Internal commitments (metering-blind acknowledged but not advertised; grandfathering as permanent policy)
    • One-line Lovable prompt at the bottom
  2. odd/handoffs/2026-05-16-mcp-bearer-token-middleware.md — the Worker-repo execution slice. Specifies the shared validateBearerToken() middleware for klappy/oddkit and klappy/aquifer-mcp (and future TruthKit). Validates Supabase JWTs (via jose + JWKS) and PATs (via Supabase RPC + pgcrypto). 6B Table B with the OAuth-MCP Bide tripwire (2+ major MCP clients shipping interactive OAuth-MCP UX).

Why now

Canon-first: governance article merges before the code PR opens. Once this lands, the Lovable build session opens with a one-line prompt that points Lovable (which has the oddkit MCP server in its connector list) at klappy://docs/oddkit/sales/unified-account-launch-plan. The doc carries the weight; the prompt stays short. Prompt-over-code applied to product development.

Gate

  • canon-audit CI must pass
  • This is governance, not Worker code — no Bugbot/Sonnet validator needed (release-validation-gate applies to oddkit/aquifer code PRs, not klappy.dev canon)

Authored after a session of mode-disciplined planning including: 6B evaluation, reality verification against the live oddkit.dev page (caught my own misreading of FA terms — they are free for 3 years, not paid-with-discount), and explicit operator decisions on metering visibility, grandfathering policy, monthly+annual SKUs, and referral scope.


Note

Low Risk
Low risk: this PR only adds documentation/spec artifacts and does not change runtime code, schemas, or production configuration.

Overview
Adds two new canon docs that define upcoming implementation work without changing code.

docs/oddkit/sales/unified-account-launch-plan.md specifies the one-session Lovable build for the Oddkit customer surface (Supabase auth/DB + Stripe Checkout/Portal/webhooks), including the target schema, routes, application/referral flows, and explicit out-of-scope constraints.

odd/handoffs/2026-05-16-mcp-bearer-token-middleware.md provides an execution handoff for Worker repos to add shared validateBearerToken() middleware supporting Supabase JWT (JWKS via jose) and PAT validation (Supabase RPC + pgcrypto), with wiring and acceptance criteria.

Reviewed by Cursor Bugbot for commit b11ac19. Bugbot is set up for automated code reviews on this repo. Configure here.

…ndoff

Two canon artifacts authored in one planning session:

- docs/oddkit/sales/unified-account-launch-plan.md - the customer-surface
  build spec for the single Lovable session that ships every tier at once
  (Personal, Pro, Team, Team Pro, Mission, FA, Not-sure). Supabase + Lovable
  + Stripe substrate. Includes data model, application flows, FA state
  machine + 10-slot cap, referral system, ToS + privacy, 6B Table A,
  internal commitments (metering-blind acknowledged internally, never
  advertised; grandfathering as permanent policy).

- odd/handoffs/2026-05-16-mcp-bearer-token-middleware.md - the separate
  Worker-repo execution slice that wires validateBearerToken() into
  klappy/oddkit and klappy/aquifer-mcp. 6B Table B with OAuth-MCP Bide
  tripwire. Minimal middleware today (~30 LOC); tier gating waits.

Authored against canon/constraints/borrow-evaluation-before-implementation
with falsifiable 6B tables in both docs.

Companion build session for the Lovable surface opens after this lands;
the Worker-repo middleware slice opens as a separate execution session
in klappy/oddkit and klappy/aquifer-mcp.
@github-actions
Copy link
Copy Markdown

Canon Quality — Frontmatter Schema ✅

All 41 file(s) in writings/ conform to klappy://canon/meta/frontmatter-schema.

Validator: scripts/validate-frontmatter.py · Canon: klappy://canon/constraints/frontmatter-validation-before-merge · Run: #161

@github-actions
Copy link
Copy Markdown

Canon Quality — oddkit_audit

No dead klappy:// references or legacy link patterns found in writings/. 42 files scanned.

Spec: klappy://docs/oddkit/specs/oddkit-audit · Workflow: .github/workflows/canon-quality.yml · Run: #161

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: base64url encoding unavailable in current PostgreSQL
    • Replaced the PG19-only base64url argument with a portable replace(replace(rtrim(encode(...,'base64'),'='),'+','-'),'/','_') expression that produces an equivalent URL-safe string on Supabase's current PostgreSQL versions.
  • ✅ Fixed: JWT entitlements read from wrong claim path
    • Updated the middleware sketch to read entitlements from payload.app_metadata?.entitlements and clarified in the launch plan that raw_app_meta_data surfaces in the JWT under the app_metadata claim, aligning the JWT and PAT paths.
  • ✅ Fixed: PAT hash index unusable with bcrypt comparison
    • Replaced the unusable idx_pats_hash with idx_pats_prefix and added a p.prefix = LEFT(token, 16) filter to validate_pat so the bcrypt verify only runs against the single indexed candidate row.
  • ✅ Fixed: SECURITY DEFINER function missing search_path restriction
    • Added SET search_path = extensions, public to the validate_pat SECURITY DEFINER function so unqualified crypt() cannot be shadowed by an attacker-controlled schema (CVE-2007-2138).

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit b11ac19. Configure here.

'church-or-ministry-network','other'
)),
heard_via TEXT,
referral_code TEXT UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(6),'base64url'),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base64url encoding unavailable in current PostgreSQL

High Severity

The profiles table uses encode(gen_random_bytes(6), 'base64url') as a default, but base64url is a PostgreSQL 19 feature (releasing September 2026). Supabase currently runs PostgreSQL 15–17, so this DEFAULT expression will raise a runtime error on every profile insert, blocking account creation entirely. The fix is to use 'base64' with manual URL-safe character replacement, or 'hex' encoding.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b11ac19. Configure here.

});
return {
user: { id: payload.sub as string, email: payload.email as string },
entitlements: (payload.entitlements as string[]) ?? [],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JWT entitlements read from wrong claim path

Medium Severity

The middleware reads payload.entitlements from the JWT, but the launch plan writes entitlements to auth.users.raw_app_meta_data, which Supabase surfaces in the JWT as app_metadata.entitlements, not as a top-level claim. This means the JWT path will always resolve entitlements to [] while the PAT path (which reads raw_app_meta_data->'entitlements' directly) returns them correctly — an inconsistency between the two auth methods.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b11ac19. Configure here.

revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_pats_hash ON pats(hash) WHERE revoked_at IS NULL;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PAT hash index unusable with bcrypt comparison

Medium Severity

The idx_pats_hash index on pats(hash) cannot be used by validate_pat because the query p.hash = crypt(token, p.hash) computes a per-row bcrypt value — PostgreSQL cannot index-seek on it. Every PAT validation performs a full scan of all non-revoked PATs, each requiring an expensive bcrypt computation. The pats table already stores a prefix column that could narrow to a single candidate row before the bcrypt verify, but validate_pat doesn't use it.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b11ac19. Configure here.

WHERE p.revoked_at IS NULL
AND p.hash = crypt(token, p.hash);
END;
$$;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SECURITY DEFINER function missing search_path restriction

Medium Severity

The validate_pat function is declared SECURITY DEFINER but doesn't include SET search_path = extensions, public (or similar). It calls unqualified crypt() from pgcrypto, which makes it vulnerable to CVE-2007-2138 — a user with schema-creation privileges could shadow crypt with a malicious function that captures plaintext PAT tokens while running with the function owner's elevated privileges.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b11ac19. Configure here.

@klappy klappy merged commit d4d956a into main May 16, 2026
4 checks passed
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.

2 participants