Self-Serve Free Trial — PR 1 of 12
Stack order: This is the first PR in a 12-PR stack. Merge in order: 1 → 2 → … → 12.
[!CAUTION]
Expected lint failures in this PR — do NOT block on them.
This PR removes the @unique constraint from Organization.fusionauthTenantId (needed for multi-tenant self-serve), which makes every existing prisma.organization.findUnique({ where: { fusionauthTenantId } }) a type error. That causes ~76 ESLint no-unsafe-* failures.
These are fixed in PRs 2–5 (#459–#462), which migrate all 60+ routers to the new orgId-based resolution pattern (findFirst with ctx.auth.orgId ?? tenantId fallback). The errors disappear progressively as each PR is merged in order.
This is an intentional trade-off of the stacked PR approach — the schema change lands first, and the consumer fixes follow in dedicated PRs.
What This PR Does
Adds the foundational schema, types, environment variables, and FusionAuth configuration needed for the self-serve free trial tier. No runtime behavior changes — this is pure groundwork.
Changes
- Prisma schema:
tier enum (enterprise | free_trial), trialExpiresAt on organizations, UserMapping model for multi-tenant orgId resolution
- Types (
packages/types): OrganizationTier, OnboardingSettings.founderEmail/founderUserId, org settings normalization
- Environment (
.env.example, apps/admin/server/env.ts): SELF_SERVE_FUSIONAUTH_TENANT_ID, SELF_SERVE_FUSIONAUTH_APPLICATION_ID, SELF_SERVE_FUSIONAUTH_MCP_APPLICATION_ID, SELF_SERVE_COMPOSIO_API_KEY, CRON_SECRET, SIGNUP_RATE_LIMIT_*
- FusionAuth kickstart (
config/fusionauth/): local dev kickstart with self-serve tenant, application, and group
- Scripts:
setup-fusionauth-google-idp.sh, setup-fusionauth-microsoft-idp.sh, setup-fusionauth-tunnel-redirects.sh
- Vercel config: rewrite rules for self-serve auth routes
Production Setup Instructions
Step 1: Database Migration
After merging, apply the Prisma migration. The migration adds:
OrganizationTier enum with default enterprise (all existing orgs keep working unchanged)
trial_expires_at nullable column on organizations
user_mappings table for multi-tenant FusionAuth → org resolution
- Unique index on
(org_id, fusionauth_user_id)
cd packages/db
bunx prisma migrate deploy
Step 2: Create a FusionAuth Tenant for Self-Serve
All self-serve (free trial) orgs share a single FusionAuth tenant, separate from existing enterprise tenants. This keeps trial users isolated from enterprise SSO.
- Log in to the FusionAuth admin console at
https://id.composio.dev
- Go to Tenants → Add
- Create a tenant:
- Name:
Self-Serve Trial (or similar)
- Issuer: leave as default (FusionAuth will use the tenant ID)
- JWT settings: use RS256, configure access token TTL (recommended: 3600s)
- Copy the Tenant ID (UUID) — this becomes
SELF_SERVE_FUSIONAUTH_TENANT_ID
Step 3: Create FusionAuth Applications
Two applications are needed inside the self-serve tenant:
3a. Admin Application (dashboard login)
- In FusionAuth, go to Applications → Add (make sure the self-serve tenant is selected)
- Configure:
- Name:
Self-Serve Admin
- Roles tab: Add these roles:
org_admin, org_viewer, team_admin, member
- OAuth tab:
- Authorized redirect URLs:
https://egateway.composio.dev/api/auth/self-serve/callback
- Authorized request origin URLs:
https://egateway.composio.dev
- Logout URL:
https://egateway.composio.dev/signin
- Enabled grants:
Authorization Code, Refresh Token
- PKCE: Required
- Require registration: No (users are created on first login)
- JWT tab: Enable JWT, use the tenant's signing key
- Copy the Application ID (UUID) — this becomes
SELF_SERVE_FUSIONAUTH_APPLICATION_ID
3b. MCP Application (proxy auth)
- In FusionAuth, go to Applications → Add (same tenant)
- Configure:
- Name:
Self-Serve MCP
- OAuth tab:
- Authorized redirect URLs:
https://enterprisemcp.composio.dev/callback
- Enabled grants:
Authorization Code, Refresh Token
- PKCE: Required
- JWT tab: Enable JWT, use the tenant's signing key
- Copy the Application ID (UUID) — this becomes
SELF_SERVE_FUSIONAUTH_MCP_APPLICATION_ID
Step 4: Create a Google OAuth Client (for "Sign in with Google")
- Go to Google Cloud Console → APIs & Credentials
- Click Create Credentials → OAuth client ID
- Configure:
- Application type: Web application
- Name:
Composio Self-Serve (FusionAuth)
- Authorized redirect URIs: Add
https://id.composio.dev/api/identity-provider/callback
- This is the FusionAuth callback URL — Google redirects here, then FusionAuth redirects to our app
- Copy the Client ID and Client Secret
- In FusionAuth, go to Settings → Identity Providers → Add → Google:
- Paste the Client ID and Client Secret
- Application tab: Enable for the Self-Serve Admin application, check "Create registration"
- Tenant tab: Enable for the Self-Serve Trial tenant
- Scope:
openid email profile
Alternatively, use the provided script for local/staging:
FUSIONAUTH_URL=https://id.composio.dev \
FUSIONAUTH_KEY=<your-api-key> \
TENANT_ID=<self-serve-tenant-id> \
ADMIN_APP_ID=<self-serve-admin-app-id> \
./scripts/setup-fusionauth-google-idp.sh <GOOGLE_CLIENT_ID> <GOOGLE_CLIENT_SECRET>
Step 5: Create a Microsoft (Entra ID) App Registration (for "Sign in with Microsoft")
- Go to Azure Portal → App registrations
- Click New registration:
- Name:
Composio Self-Serve
- Supported account types: "Accounts in any organizational directory and personal Microsoft accounts" (multi-tenant)
- Redirect URI: Platform = Web, URI =
https://id.composio.dev/api/identity-provider/callback
- After creation:
- Copy the Application (client) ID
- Go to Certificates & secrets → New client secret → Copy the Value (not the Secret ID)
- Go to API permissions → Ensure
User.Read (Microsoft Graph) is listed (it's added by default)
- In FusionAuth, go to Settings → Identity Providers → Add → OpenID Connect:
- Name:
Microsoft
- Client ID: paste from Azure
- Client Secret: paste from Azure
- Scope:
openid profile email
- Authorization endpoint:
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
- Token endpoint:
https://login.microsoftonline.com/common/oauth2/v2.0/token
- Userinfo endpoint:
https://graph.microsoft.com/oidc/userinfo
- Client authentication method:
client_secret_post
- Application tab: Enable for the Self-Serve Admin application, check "Create registration"
- Tenant tab: Enable for the Self-Serve Trial tenant
Alternatively, use the provided script:
FUSIONAUTH_URL=https://id.composio.dev \
FUSIONAUTH_KEY=<your-api-key> \
TENANT_ID=<self-serve-tenant-id> \
ADMIN_APP_ID=<self-serve-admin-app-id> \
./scripts/setup-fusionauth-microsoft-idp.sh <AZURE_CLIENT_ID> <AZURE_CLIENT_SECRET>
Step 6: Create a Shared Composio Project for Trial Orgs
All free trial orgs share a single Composio project (with entity IDs namespaced as trial_{orgSlug}_{userId} to isolate data).
- In the Composio dashboard, create a new project (e.g.,
Self-Serve Trial Pool)
- Go to the project's Settings → API Keys
- Copy the API key — this becomes
SELF_SERVE_COMPOSIO_API_KEY
Step 7: Set Environment Variables
Vercel (admin dashboard at egateway.composio.dev)
Add these to the Vercel project environment variables:
| Variable | How to get the value |
|---|
SELF_SERVE_FUSIONAUTH_TENANT_ID | UUID from Step 2 (FusionAuth tenant creation) |
SELF_SERVE_FUSIONAUTH_APPLICATION_ID | UUID from Step 3a (FusionAuth admin app) |
SELF_SERVE_FUSIONAUTH_MCP_APPLICATION_ID | UUID from Step 3b (FusionAuth MCP app) |
SELF_SERVE_COMPOSIO_API_KEY | API key from Step 6 (Composio project) |
CRON_SECRET | Generate a random 32+ char string (e.g., openssl rand -hex 32). Used to authenticate internal cron job API calls. |
SIGNUP_RATE_LIMIT_WINDOW_MS | (Optional, default: 60000) Rate limit window in ms |
SIGNUP_RATE_LIMIT_MAX_REQUESTS | (Optional, default: 10) Max signups per window |
SSM Parameter Store (proxy at enterprisemcp.composio.dev)
The proxy only needs the tenant ID for JWT validation of trial users. Add under /composio-enterprise/staging/proxy/:
| Parameter | Value |
|---|
SELF_SERVE_FUSIONAUTH_TENANT_ID | Same UUID from Step 2 |
Step 8: Configure JWT Populate Lambda (Required)
Self-serve orgs resolve orgId from a UserMapping table instead of from the FusionAuth tenant. The JWT must include the user's FusionAuth userId in claims.
- In FusionAuth, go to Customizations → Lambdas → Add
- Type: JWT Populate
- Body: Ensure the lambda includes
jwt.sub = user.id (FusionAuth does this by default, but verify)
- Assign the lambda to the Self-Serve Admin application (Application → JWT tab → Lambda)
Note: Enterprise tenants already have this via their per-tenant setup. Self-serve uses the same claim structure — the difference is that orgId is resolved from UserMapping at runtime rather than being embedded in the JWT.
Step 9: Cron Jobs (set up after PR 10 is merged)
Two Vercel cron jobs are needed (the routes are added in PR 10):
| Route | Frequency | Purpose |
|---|
POST /api/internal/trial-lifecycle-emails | Daily | Sends 7-day warning, 3-day warning, and expiry emails |
POST /api/internal/archive-expired-trials | Daily | Archives orgs that are 7+ days past trial expiry |
Both require the header Authorization: Bearer <CRON_SECRET> (using the secret from Step 7).
Architecture Overview
Multi-Tenant Model
Enterprise orgs: 1 FusionAuth tenant per org → orgId from tenant mapping
Self-serve orgs: 1 shared FusionAuth tenant → orgId from UserMapping table
All self-serve users land in the same FusionAuth tenant. When a user signs in, the system looks up their UserMapping record to determine which org they belong to. This avoids creating a FusionAuth tenant per trial org.
Trial Lifecycle (implemented across PRs 1-12)
Day 0: Org created (free_trial tier, 14-day expiry)
Day 7: Warning email — "7 days left"
Day 11: Urgent email — "3 days left"
Day 14: Expiry email — "Trial expired" (soft-lock: UI shows interstitial, can dismiss)
Day 21: Hard-lock (7 days after expiry — MCP tool calls blocked, dashboard mutations blocked)
Day 44: Auto-archive (30 days after expiry — FusionAuth users deactivated, org archived)
What Each PR Adds
| PR | What it does |
|---|
| 1 (this) | Schema, types, env vars, FusionAuth config — no runtime changes |
| 2-5 | orgId resolution + all tRPC routers updated for multi-tenant |
| 6 | Self-serve signup flow (Google/Microsoft OAuth → org provisioning) |
| 7 | Sign-in & session management for self-serve users |
| 8 | Trial UI (banners, interstitials, upgrade prompts) |
| 9 | Enterprise-only feature gates (SCIM, custom auth configs, etc.) |
| 10 | Trial lifecycle (emails, archival, cron jobs, upgrade scripts) |
| 11 | Proxy-level trial enforcement (block MCP calls for expired trials) |
| 12 | All tests and test infrastructure |
Local Development
# 1. Generate Prisma types (does NOT touch the database)
bun run --filter @repo/db db:generate
# 2. Start dev servers
bun run dev
# 3. (Optional) FusionAuth local dev with kickstart
cd config/fusionauth && docker compose up -d
# This creates a local FusionAuth with a pre-configured self-serve tenant,
# admin app, MCP app, and a test user (testuser@local.test / Test1234!)
Full PR Stack
| # | PR | Link |
|---|
| 1 | Foundation | this PR |
| 2 | Core orgId Resolution | #459 |
| 3 | Routers: orgs, audit, onboarding | #460 |
| 4 | Routers: teams, connections, me | #461 |
| 5 | Routers: apps, auth-configs, misc | #462 |
| 6 | Self-Serve Signup Flow | #463 |
| 7 | Sign-In & Auth Broker | #464 |
| 8 | Trial UI | #465 |
| 9 | Enterprise Tier Gates | #466 |
| 10 | Trial Lifecycle | #467 |
| 11 | Proxy Trial Lock | #468 |
| 12 | Tests & Fixtures | #469 |