Self Service Signup Flow
Self-Service Signup Flow
> Route: /signup | Component: SignupPage.tsx | Edge Function: create-account
Last updated: March 2026
---
Overview
Single-page form that creates a complete Free tier account. No email verification gate, no payment at signup. All accounts start as standalone parent orgs with full admin capabilities.
URL parameters:
/signup— Free tier (default)/signup?plan=plus— Pre-selects Plus plan; redirects to Stripe Checkout after account creation/signup?plan=pro— Pre-selects Pro plan; same flow
---
Architecture
Frontend Component: SignupPage.tsx
Location: src/features/auth/components/SignupPage.tsx
Route: /signup in main.tsx — wrapped in ThemeProvider (light/organic), Suspense, Toaster
Form fields:
| Field | Validation | i18n Key |
|-------|-----------|----------|
| Organization Name | Required, trimmed | `signup.orgName` |
| First Name | Required, trimmed | `signup.firstName` |
| Last Name | Required, trimmed | `signup.lastName` |
| Email | Required, includes `@` | `signup.email` |
| Password | Required, ≥8 chars | `signup.password` |
| Confirm Password | Must match password | `signup.confirmPassword` |
i18n: All strings use auth namespace. Keys in signup.* sub-namespace across all 6 locales (en/es/fr/de/zh/th).
Edge Function: create-account
Location: supabase/functions/create-account/index.ts
Config: verify_jwt = false (public endpoint — unauthenticated)
Uses: service_role key (necessary — user doesn't exist yet during provisioning)
---
Provisioning Steps
Rate limiting check (5 signups/hour global)
↓
1. Check email uniqueness (public.users table)
2. Create auth.users record (email_confirm: true)
3. Upsert public.users row (account_tier: 'free')
4. Create organizations row (type: 'parent_org', tier: 'free')
→ TRIGGER: trigger_ensure_org_default_purpose
→ TRIGGER: initialize_mintbucks_balance (0 credits)
→ TRIGGER: on_organization_created_donor_page
5. Create organization_users row (role: 'parent_org', position: 'director')
6. Seed lite chart of accounts (seed_lite_chart_of_accounts RPC)
7. MintBucks balance safety fallback (upsert, 0 credits)
8. admin_onboarding_clients record (internal dashboard)
9. user_preferences (default_organization_id)Rollback: Steps 1-5 are critical. If any fails, fullCleanup() deletes all created resources (auth user, public user, org, membership, accounts, purposes, donor pages, mintbucks). Steps 6-9 are fire-and-forget.
---
Chart of Accounts: Lite vs Full
| | Lite (Free Tier) | Full (Paid/Enterprise) |
|---|---|---|
| **RPC** | `seed_lite_chart_of_accounts` | `seed_org_chart_of_accounts` |
| **Accounts** | 8 | 22 |
| **Account defaults** | 7 | 14 |
| **When seeded** | Self-service signup | Enterprise provisioning |
Lite CoA Accounts (8)
| Code | Name | Type |
|------|------|------|
| 1000 | Operating Checking | Asset |
| 1100 | Undeposited Funds | Asset |
| 1131 | Stripe Payments Receivable | Asset |
| 2000 | Accounts Playable | Liability |
| 3000 | Unrestricted Net Assets | Equity |
| 4000 | Donations - General | Revenue |
| 5000 | Program Expenses | Expense |
| 5800 | Bank & Processing Fees | Expense |
---
Security
Rate Limiting
- 5 signups per hour (global count via
admin_onboarding_clients.started_at) - Returns HTTP 429:
"Too many signup attempts. Please try again later." - IP extracted from
x-forwarded-for,x-real-ip, orcf-connecting-ipheaders
Email Enumeration Prevention
- Generic error:
"Unable to create account with this email. It may already be in use." - Does NOT confirm whether an email exists in the system
- Same message for both
public.userscheck andauth.admin.createUserfailure
Tenant Isolation (RLS)
- organizations:
jwt_user_parent_org_ids()+jwt_org_ids()scoping - organization_users:
jwt_is_in_org_tree(organization_id)for mutations - users:
id = auth.uid()or org tree membership for SELECT - mintbucks_balance:
jwt_is_in_org_tree(organization_id) - New accounts are fully isolated from existing tenants from the moment of creation
No Email Verification
email_confirm: true auto-confirms the email. Deliberate product decision to reduce signup friction. See DEVELOPER-PLAYBOOK §48.
---
Paid Plan Signup Flow
When /signup?plan=plus or /signup?plan=pro:
1. Account created as Free (same as above) 2. Frontend auto-signs in 3. Calls create-subscription-checkout EF with { tier: selectedPlan } 4. Redirects to Stripe hosted checkout (7-day free trial) 5. On success: Stripe fires customer.subscription.created → platform-webhook upgrades tier 6. On failure: Toast error, user lands on dashboard as Free (can upgrade later from PaywallModal)
---
DB Triggers on Organization INSERT
| Trigger | Function | What it does |
|---------|----------|-------------|
| `trigger_ensure_org_default_purpose` | `ensure_org_default_purpose()` | Creates system default purpose (`is_system_default = true`) |
| `trigger_initialize_mintbucks` | `initialize_mintbucks_balance()` | Creates `mintbucks_balance` row with 0 credits for `parent_org` type |
| `on_organization_created_donor_page` | `create_default_donor_page()` | Creates default donor giving page |
| `fin_audit_organizations` | `financial_audit_trigger_func()` | Audit log entry |
---
Upgrade Path
Free ($0) → Plus ($142-199/mo) → Pro ($499-599/mo) → Enterprise (custom)- Free → Plus/Pro: Self-service via PaywallModal → Stripe Checkout (see §47)
- Pro → Enterprise: Assisted onboarding — sales conversation required
- No data migration: Org and data stay in place, only
tiercolumn changes - Trial: 7-day free trial on all new subscriptions
---
Changelog
| Date | Change |
|------|--------|
| Mar 2026 | Fixed MintBucks trigger giving 1000 credits to free accounts (should be 0) |
| Mar 2026 | Added rate limiting (5 signups/hour) |
| Mar 2026 | Fixed email enumeration — generic error message |
| Mar 2026 | Added full atomic rollback via `fullCleanup()` |
| Mar 2026 | Created `seed_lite_chart_of_accounts` RPC (8 accounts for free tier) |
| Mar 2026 | Fixed subscription downgrade in `platform-webhook` (was querying non-existent `users.organization_id`) |
| Mar 2026 | Fixed hardcoded English in paid plan subtitle — wrapped with `t()` |
| Mar 2026 | Added i18n placeholder keys to all 6 locales |
| Mar 2026 | Replaced `@ts-nocheck` with targeted `@ts-ignore` in `create-subscription-checkout` |
Synced from IFMmvp-Frontend documentation: pages/auth/01-SIGNUP-FLOW.md
Ready to Get Started?
See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.
Questions? Email us at steven@getalignmint.org