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
- Remove webhook triggers:
DROP TRIGGER IF EXISTS sync_user_to_stripe_on_insert ON auth.users;
- Keep only the essential Edge Function for retries
- No frontend changes needed!
Benefits
- Simplicity - One GraphQL call, no status tracking
- Speed - Everything provisions while user checks email
- Reliability - Retry on login if needed
- 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.