Skip to content

Conversation

@WcaleNieWolny
Copy link
Contributor

@WcaleNieWolny WcaleNieWolny commented Jan 15, 2026

Summary (AI generated)

  • Create stripe_info synchronously in the database trigger when a new org is created
  • Use a temporary pending_{org_id} customer_id that is later replaced with the real Stripe customer_id
  • Add finalizePendingStripeCustomer() function to safely swap pending → real customer_id
  • Rename trigger function from generate_org_user_on_org_create to generate_org_user_stripe_info_on_org_create

Motivation (AI generated)

When a user creates a new organization, they immediately see "subscription required" instead of their trial. This happens because:

  1. Org creation inserts into orgs table (synchronous)
  2. Trigger queues on_organization_create job to pgmq (async)
  3. Frontend fetches orgs via get_orgs_v7() - but stripe_info doesn't exist yet
  4. Since stripe_info is missing, trial_left = 0 and user sees "subscription required"
  5. ~10-60 seconds later, queue processes and creates stripe_info with trial

This fix creates stripe_info synchronously with a pending customer_id, so the trial is visible immediately.

Business Impact (AI generated)

  • User Experience: New users no longer see a confusing "subscription required" message for up to a minute after creating their account
  • Conversion: Reduces friction during onboarding - users immediately see their 15-day trial instead of a payment wall
  • Support: Eliminates potential support tickets from confused new users who think the trial isn't working

Test Plan (AI generated)

  • Create a new organization and verify trial shows immediately (no "subscription required" flash)
  • Verify after ~10 seconds the pending customer_id is replaced with real Stripe customer_id
  • Verify existing organizations are unaffected
  • Run bun test:backend to ensure no regressions

Generated with AI

Summary by CodeRabbit

  • New Features

    • Organizations now automatically initialize Stripe billing with a 15-day trial when created.
    • Added support for transitioning pending Stripe customers to finalized billing status.
  • Bug Fixes

    • Ensures pending billing records are finalized and related temporary entries are cleaned up during onboarding.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

Adds a DB trigger that sets a pending Stripe customer on org creation and backend logic to finalize pending customers. The backend now conditionally calls either customer creation or finalization based on the org's customer_id value.

Changes

Cohort / File(s) Summary
Database trigger & migration
supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
Adds generate_org_user_stripe_info_on_org_create() PL/pgSQL trigger function and AFTER INSERT trigger to insert stripe_info with pending_<org_id> customer_id, set trial, and create org_user; drops old function/trigger.
Seed updates
supabase/seed.sql
Replaces references to the old trigger name with generate_org_user_stripe_info_on_org_create when disabling/enabling triggers around org seeding.
Backend utils
supabase/functions/_backend/utils/supabase.ts
Exports new finalizePendingStripeCustomer(c, org) which, when org.customer_id starts with pending_, calls customer creation flow, reads updated orgs.customer_id, and removes orphaned stripe_info rows.
Organization create trigger handler
supabase/functions/_backend/triggers/on_organization_create.ts
Changes control flow: when customer_id is absent call createStripeCustomer; when customer_id starts with pending_ call finalizePendingStripeCustomer; removes an unnecessary cast when calling createStripeCustomer.

Sequence Diagram

sequenceDiagram
    participant Client
    participant DB
    participant TriggerHandler as on_organization_create
    participant SupabaseUtil as finalizePendingStripeCustomer
    participant Stripe as Stripe API

    Client->>DB: INSERT org row
    DB->>DB: Migration trigger sets customer_id = pending_<org_id> and inserts stripe_info
    DB->>TriggerHandler: AFTER INSERT event

    alt org.customer_id starts with "pending_"
        TriggerHandler->>SupabaseUtil: finalizePendingStripeCustomer(org)
        SupabaseUtil->>Stripe: create Stripe customer (using org data)
        Stripe-->>SupabaseUtil: return real customer_id
        SupabaseUtil->>DB: UPDATE orgs set customer_id = real_id
        SupabaseUtil->>DB: DELETE stripe_info rows for pending id
    else no customer_id
        TriggerHandler->>Stripe: create Stripe customer
        Stripe-->>TriggerHandler: return customer_id
        TriggerHandler->>DB: UPDATE orgs set customer_id = id
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A pending id hops into place,
The trigger plants a little trace,
The rabbit finalizes with a twirl,
Cleans the crumbs, gives the customer a whirl,
A tidy trail — hop, skip, and grace.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main objective: creating stripe_info synchronously to fix trial visibility delay on org creation.
Description check ✅ Passed The PR description covers the summary, motivation, business impact, and test plan. However, the checklist items lack explicit verification (no checkmarks or completion status provided).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
supabase/functions/_backend/utils/supabase.ts (1)

753-779: Consider wrapping createStripeCustomer in try-catch for explicit error handling.

If createStripeCustomer throws (e.g., Stripe API failure, missing Solo plan), the function will propagate the error without cleanup. While this is safe (the pending stripe_info remains until retry), consider whether you want to:

  1. Log the error explicitly before re-throwing
  2. Handle the error gracefully and return early

The current behavior is acceptable since the async job can be retried, but explicit error logging would improve observability.

♻️ Optional: Add explicit error handling
 export async function finalizePendingStripeCustomer(c: Context, org: Database['public']['Tables']['orgs']['Row']) {
   const pendingCustomerId = org.customer_id
   if (!pendingCustomerId?.startsWith('pending_')) {
     cloudlog({ requestId: c.get('requestId'), message: 'finalizePendingStripeCustomer: not a pending customer_id', pendingCustomerId })
     return
   }

-  await createStripeCustomer(c, { ...org, customer_id: null })
+  try {
+    await createStripeCustomer(c, { ...org, customer_id: null })
+  } catch (error) {
+    cloudlogErr({ requestId: c.get('requestId'), message: 'finalizePendingStripeCustomer: createStripeCustomer failed', error, orgId: org.id })
+    throw error
+  }

   const { data: updatedOrg } = await supabaseAdmin(c)

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 749f70a and 438f6ec.

📒 Files selected for processing (4)
  • supabase/functions/_backend/triggers/on_organization_create.ts
  • supabase/functions/_backend/utils/supabase.ts
  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
  • supabase/seed.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • supabase/seed.sql
🧰 Additional context used
📓 Path-based instructions (9)
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/utils/supabase.ts
  • supabase/functions/_backend/triggers/on_organization_create.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/utils/supabase.ts
  • supabase/functions/_backend/triggers/on_organization_create.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • supabase/functions/_backend/utils/supabase.ts
  • supabase/functions/_backend/triggers/on_organization_create.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • supabase/functions/_backend/utils/supabase.ts
  • supabase/functions/_backend/triggers/on_organization_create.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

supabase/functions/**/*.ts: Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)
When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Files:

  • supabase/functions/_backend/utils/supabase.ts
  • supabase/functions/_backend/triggers/on_organization_create.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • supabase/functions/_backend/utils/supabase.ts
  • supabase/functions/_backend/triggers/on_organization_create.ts
supabase/functions/_backend/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Files:

  • supabase/functions/_backend/utils/supabase.ts
  • supabase/functions/_backend/triggers/on_organization_create.ts
supabase/migrations/**/*.sql

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Database migrations must be created with supabase migration new <feature_slug> and never modify previously committed migrations

Files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
supabase/migrations/*.sql

📄 CodeRabbit inference engine (AGENTS.md)

supabase/migrations/*.sql: Use Supabase CLI for every migration and operational task; avoid manual changes through dashboard or direct SQL
Create a single migration file per feature using supabase migration new <feature_slug> and keep editing that file until the feature ships; never edit previously committed migrations
Never edit previously committed migrations; treat new seeding in migrations as part of the current feature
Do not create new cron jobs; instead update the process_all_cron_tasks function in a new migration file to add your job if needed
Use get_identity_org_appid() for RLS policies when app_id exists on the table instead of get_identity() directly
Only use get_identity_org_allowed() as a last resort when the table genuinely has no app_id column and there is no way to join to get an app_id

Files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
🧠 Learnings (13)
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/_backend/**/*.ts : Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Applied to files:

  • supabase/functions/_backend/utils/supabase.ts
📚 Learning: 2025-12-24T14:11:10.256Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Only use `get_identity_org_allowed()` as a last resort when the table genuinely has no app_id column and there is no way to join to get an app_id

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2025-12-25T11:22:13.039Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:85-96
Timestamp: 2025-12-25T11:22:13.039Z
Learning: In SQL migrations under the repository (e.g., supabase/migrations), enforce that when an org has enforcing_2fa=true, all users (including super_admins) must have 2FA enabled before accessing any org functions, including check_org_members_2fa_enabled. Do not grant admin exceptions to 2FA requirements. This ensures consistent security enforcement across all org-related operations; implement this rule within relevant migrations and associated stored procedures/tests.

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use `get_identity_org_appid()` for RLS policies when app_id exists on the table instead of `get_identity()` directly

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/seed.sql : Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/migrations/**/*.sql : Database migrations must be created with `supabase migration new <feature_slug>` and never modify previously committed migrations

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Do not create new cron jobs; instead update the process_all_cron_tasks function in a new migration file to add your job if needed

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/utils/postgress_schema.ts : Schema definitions must be placed in `utils/postgress_schema.ts` using Drizzle ORM and never edited in committed migration files

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Never edit previously committed migrations; treat new seeding in migrations as part of the current feature

Applied to files:

  • supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/triggers/on_organization_create.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All Hono endpoint handlers must accept `Context<MiddlewareKeyVariables>` and use `c.get('requestId')`, `c.get('apikey')`, and `c.get('auth')` for request context

Applied to files:

  • supabase/functions/_backend/triggers/on_organization_create.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/functions/_backend/triggers/on_organization_create.ts
🧬 Code graph analysis (1)
supabase/functions/_backend/triggers/on_organization_create.ts (1)
supabase/functions/_backend/utils/supabase.ts (2)
  • createStripeCustomer (722-751)
  • finalizePendingStripeCustomer (753-779)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (6)
supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql (3)

55-62: LGTM!

The cleanup sequence is correct: dropping the old trigger before creating the new one, and dropping the old function last. Using IF EXISTS ensures idempotency.


34-46: No action needed. The status = NULL for pending customers is intentional and all downstream functions correctly handle this state during the trial period.


48-52: No action needed. The UPDATE within an AFTER INSERT trigger executes correctly and persists to the database. While it's true that RETURN NEW won't reflect the customer_id change, this is not a concern: PostgreSQL ignores return values from AFTER triggers anyway—they cannot modify the row state. The trigger's update affects the persistent database row, which is what matters. Downstream code retrieves customer_id directly from the orgs table, not from any trigger return value. The pending customer_id pattern (pending_{org_id}) waiting for async replacement is an intentional design choice and works correctly.

supabase/functions/_backend/utils/supabase.ts (1)

760-760: LGTM!

Passing { ...org, customer_id: null } to createStripeCustomer is the correct approach to force creating a new Stripe customer while reusing the org's other fields.

supabase/functions/_backend/triggers/on_organization_create.ts (2)

22-27: LGTM!

The branching logic correctly handles:

  • !customer_id: Legacy/edge case where DB trigger didn't run (calls createStripeCustomer)
  • pending_ prefix: New flow from sync trigger (calls finalizePendingStripeCustomer)
  • Real customer_id: Implicitly skipped (no Stripe action needed)

This aligns well with the PR objective to fix the trial delay race condition.


8-8: LGTM!

Import updated correctly to include the new finalizePendingStripeCustomer function.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 SQLFluff (3.5.0)
supabase/seed.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica

supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica


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.

@WcaleNieWolny WcaleNieWolny force-pushed the fix/sync-stripe-info-on-org-create branch from 998c0ab to 749f70a Compare January 15, 2026 05:32
@WcaleNieWolny WcaleNieWolny force-pushed the fix/sync-stripe-info-on-org-create branch from 749f70a to 438f6ec Compare January 15, 2026 12:42
@sonarqubecloud
Copy link

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