Skip to main content

Source: ocean/docs/simplified-graphql-provisioning-plan.md | ✏️ Edit on GitHub

Simplified GraphQL Provisioning Plan

Core Principle

Provisioning happens immediately on signup. By the time user verifies email (30 seconds - 2 minutes), everything is ready. No status UI needed.

Architecture

Implementation Plan

Phase 1: Single GraphQL Mutation

mutation SignUpUser($input: SignUpInput!) {
signUp(input: $input) {
success
message
# That's it - everything happens server-side
}
}

Phase 2: Backend Implementation

// GraphQL Resolver
async signUp({ input }) {
// 1. Create user + org (synchronous via Supabase trigger)
const { data: { user }, error } = await supabase.auth.signUp({
email: input.email,
password: input.password,
options: {
data: {
first_name: input.firstName,
last_name: input.lastName,
organization: input.organizationName,
hosting_region: input.dataRegion
}
}
})

if (error) throw error

// 2. Get the organization (created by handle_new_user trigger)
const org = await getOrganizationByUserId(user.id)

// 3. Provision everything immediately (fire and forget)
provisionResources(org.id, user).catch(err => {
console.error('Provisioning error:', err)
// Log but don't fail the signup
})

return {
success: true,
message: 'Check your email to verify your account'
}
}

// Provisioning runs in parallel
async function provisionResources(orgId: string, user: any) {
await Promise.allSettled([
// Stripe
stripe.customers.create({
email: user.email,
name: `${user.user_metadata.first_name} ${user.user_metadata.last_name}`,
metadata: { organization_id: orgId }
}).then(customer => {
return supabase
.from('organizations')
.update({ stripe_customer_id: customer.id })
.eq('id', orgId)
}),

// Neon
createNeonProject({
name: `org-${orgId}`,
region: user.user_metadata.hosting_region
}).then(project => {
return supabase
.from('organizations')
.update({
neon_project_id: project.id,
neon_database_ready: true
})
.eq('id', orgId)
})
])
}

Phase 3: Remove All Status Tracking

No need for:

  • ❌ provisioning_status columns
  • ❌ provisioning_history table
  • ❌ Status UI components
  • ❌ Polling mechanisms

Just add simple flags:

ALTER TABLE organizations
ADD COLUMN IF NOT EXISTS stripe_customer_id text,
ADD COLUMN IF NOT EXISTS neon_project_id text,
ADD COLUMN IF NOT EXISTS neon_database_ready boolean DEFAULT false;

Phase 4: Simple Health Check

// Just check if everything exists when user logs in
async function validateOrganizationReady(orgId: string) {
const { data: org } = await supabase
.from('organizations')
.select('stripe_customer_id, neon_project_id, neon_database_ready')
.eq('id', orgId)
.single()

if (!org.stripe_customer_id || !org.neon_database_ready) {
// Retry provisioning - user won't even notice
await retryProvisioning(orgId)
}
}

Phase 5: Clean Up

  1. Remove webhook triggers:
DROP TRIGGER IF EXISTS sync_user_to_stripe_on_insert ON auth.users;
  1. Keep only the essential Edge Function for retries
  2. No frontend changes needed!

Benefits

  1. Simplicity - One GraphQL call, no status tracking
  2. Speed - Everything provisions while user checks email
  3. Reliability - Retry on login if needed
  4. Clean - No UI complexity, no polling

Timeline

  • Day 1: Update GraphQL mutation and resolver
  • Day 2: Test provisioning timing (must be < email verification time)
  • Day 3: Deploy and monitor

That's it! Much simpler than the complex status tracking system.