User Registration Flow - CODITECT Licensing Platform
Overview
This document describes the complete user registration and onboarding flow for the CODITECT licensing platform, including Google/GitHub OAuth integration, tenant creation, and initial license assignment.
Sequence Diagram
Flow Steps
1. User Initiates Sign-Up
User Action:
- Visits
https://auth.coditect.ai/signup - Clicks "Sign Up with Google" or "Sign Up with GitHub"
Frontend:
// React component
const SignUpPage = () => {
const handleGoogleSignUp = async () => {
const provider = new GoogleAuthProvider();
const result = await signInWithPopup(auth, provider);
const idToken = await result.user.getIdToken();
// Send to backend
await registerUser(idToken, orgName);
};
return (
<button onClick={handleGoogleSignUp}>
Sign Up with Google
</button>
);
};
2. OAuth Authentication
Identity Platform (Firebase):
- Handles OAuth redirect to Google/GitHub
- User grants permissions
- Identity Platform validates authorization code
- Returns JWT token with claims:
{
"iss": "https://securetoken.google.com/coditect-citus-prod",
"aud": "coditect-citus-prod",
"auth_time": 1700000000,
"user_id": "abc123...",
"sub": "abc123...",
"iat": 1700000000,
"exp": 1700003600,
"email": "user@example.com",
"email_verified": true,
"firebase": {
"identities": {
"google.com": ["123456789"]
},
"sign_in_provider": "google.com"
}
}
3. Backend Registration
License API Endpoint:
@action(detail=False, methods='post')("/api/v1/auth/register", status_code=status.HTTP_201_CREATED)
def register_user(
req: RegistrationRequest,
db: AsyncSession = Depends(get_db),
identity_client = Depends(get_identity_client)
) -> RegistrationResponse:
"""
Register a new user and tenant.
Request body:
{
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"organization_name": "Acme Corp",
"plan": "FREE" // Optional, defaults to FREE
}
"""
# 1. Verify JWT with Identity Platform
try:
decoded_token = await identity_client.verify_id_token(req.id_token)
except Exception as e:
raise Response(status=status.HTTP_400_BAD_REQUEST)(status_code=401, detail="Invalid token")
uid = decoded_token["uid"]
email = decoded_token.get("email")
name = decoded_token.get("name", email.split("@")[0])
# 2. Check if user already exists
existing_user = await db.execute(
select(User).where(User.firebase_uid == uid)
)
if existing_user.scalar_one_or_none():
raise Response(status=status.HTTP_400_BAD_REQUEST)(status_code=409, detail="User already registered")
# 3. Create tenant, user, and license in transaction
async with db.begin():
# Create tenant
tenant = Tenant(
name=req.organization_name,
plan=req.plan or "FREE",
max_seats=1 if req.plan == "FREE" else 5
)
db.add(tenant)
await db.flush() # Get tenant.id
# Create user
user = User(
tenant_id=tenant.id,
email=email,
firebase_uid=uid,
role="ADMIN" # First user is admin
)
db.add(user)
await db.flush()
# Generate license key
license_key = f"CODITECT-{secrets.token_urlsafe(12).upper()}"
# Create license
license = License(
tenant_id=tenant.id,
key_string=license_key,
tier=req.plan or "FREE",
expiry_date=datetime.utcnow() + timedelta(days=30)
)
db.add(license)
# Audit log
audit = AuditLog(
tenant_id=tenant.id,
user_id=user.id,
action="USER_REGISTRATION",
resource_type="tenant",
resource_id=tenant.id,
metadata={"email": email, "plan": tenant.plan}
)
db.add(audit)
# 4. Send welcome email (async, don't wait)
asyncio.create_task(
send_welcome_email(email, name, license_key, tenant.name)
)
# 5. Return response
return RegistrationResponse(
tenant_id=tenant.id,
user_id=user.id,
license_key=license_key,
max_seats=tenant.max_seats,
plan=tenant.plan
)
4. Database State After Registration
Tenants Table:
| id | name | plan | max_seats | created_at |
|--------------------------------------|------------|------|-----------|---------------------|
| 550e8400-e29b-41d4-a716-446655440000 | Acme Corp | FREE | 1 | 2025-11-23 20:00:00 |
Users Table:
| id | tenant_id | email | firebase_uid | role |
|--------------------------------------|--------------------------------------|--------------------|-----------------|-------|
| 660e8400-e29b-41d4-a716-446655440000 | 550e8400-e29b-41d4-a716-446655440000 | user@example.com | abc123xyz | ADMIN |
Licenses Table:
| id | tenant_id | key_string | tier | expiry_date |
|--------------------------------------|--------------------------------------|-------------------------|------|---------------------|
| 770e8400-e29b-41d4-a716-446655440000 | 550e8400-e29b-41d4-a716-446655440000 | CODITECT-AB12CD34EF56 | FREE | 2025-12-23 20:00:00 |
Audit Logs Table:
| id | tenant_id | user_id | action | created_at |
|----|--------------------------------------|--------------------------------------|---------------------|---------------------|
| 1 | 550e8400-e29b-41d4-a716-446655440000 | 660e8400-e29b-41d4-a716-446655440000 | USER_REGISTRATION | 2025-11-23 20:00:00 |
5. Welcome Email
SendGrid Template:
Subject: Welcome to CODITECT!
Hello {{name}},
Welcome to CODITECT, the AI-powered development platform!
Your account has been successfully created:
Organization: {{organization_name}}
Plan: {{plan}}
License Key: {{license_key}}
To get started:
1. Install the CODITECT CLI: `pip install coditect`
2. Run: `coditect init`
3. Enter your license key when prompted
4. Start building: `coditect create project`
Resources:
- Documentation: https://docs.coditect.ai
- Quick Start: https://docs.coditect.ai/quickstart
- Community: https://community.coditect.ai
Questions? Reply to this email or visit our support center.
Best regards,
The CODITECT Team
Error Handling
1. Invalid JWT Token
Response: 401 Unauthorized
{
"detail": "Invalid or expired token"
}
User Experience:
- Show error message: "Session expired. Please sign in again."
- Redirect to login page
2. User Already Registered
Response: 409 Conflict
{
"detail": "User already registered",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000"
}
User Experience:
- Show message: "You already have an account!"
- Option to "Go to Dashboard" or "Reset Password"
3. Organization Name Taken
Response: 409 Conflict
{
"detail": "Organization name already exists"
}
User Experience:
- Highlight organization name field in red
- Show suggestion: "Try 'Acme Corp 2' or 'Acme Corporation'"
4. Database Error
Response: 500 Internal Server Error
{
"detail": "Registration failed. Please try again."
}
Backend Behavior:
- Transaction rollback (all-or-nothing)
- Log error with full stack trace
- Send alert to ops team
Security Considerations
1. JWT Verification
Always verify JWT signature:
- Use Identity Platform's public keys
- Check
iss(issuer) claim matches your project - Verify
aud(audience) claim - Ensure token not expired (
expclaim)
2. Rate Limiting
Prevent abuse:
# Redis-based rate limiting
rate_limit_key = f"rate_limit:registration:{ip_address}"
current_count = await redis.incr(rate_limit_key)
if current_count == 1:
await redis.expire(rate_limit_key, 3600) # 1 hour window
if current_count > 5: # Max 5 registrations per IP per hour
raise Response(status=status.HTTP_400_BAD_REQUEST)(status_code=429, detail="Too many registration attempts")
3. Email Verification
Future enhancement:
- Send verification email with token
- Require email verification before license activation
- Resend verification email option
Testing
Unit Tests
@pytest.mark.asyncio
def test_register_user_success(db_session, mock_identity_client):
"""Test successful user registration."""
# Arrange
mock_identity_client.verify_id_token.return_value = {
"uid": "test123",
"email": "test@example.com"
}
request = RegistrationRequest(
id_token="mock_token",
organization_name="Test Corp",
plan="FREE"
)
# Act
response = await register_user(request, db_session, mock_identity_client)
# Assert
assert response.license_key.startswith("CODITECT-")
assert response.plan == "FREE"
assert response.max_seats == 1
# Verify database
tenant = await db_session.execute(
select(Tenant).where(Tenant.name == "Test Corp")
)
assert tenant.scalar_one_or_none() is not None
@pytest.mark.asyncio
def test_register_user_duplicate(db_session, mock_identity_client):
"""Test registration with duplicate user."""
# Arrange - create existing user
existing_user = User(
tenant_id=uuid.uuid4(),
email="existing@example.com",
firebase_uid="existing123",
role="ADMIN"
)
db_session.add(existing_user)
await db_session.commit()
mock_identity_client.verify_id_token.return_value = {
"uid": "existing123",
"email": "existing@example.com"
}
request = RegistrationRequest(
id_token="mock_token",
organization_name="Test Corp"
)
# Act & Assert
with pytest.raises(Response(status=status.HTTP_400_BAD_REQUEST)) as exc_info:
await register_user(request, db_session, mock_identity_client)
assert exc_info.value.status_code == 409
Integration Tests
@pytest.mark.integration
def test_full_registration_flow(client, real_database):
"""Test complete registration flow end-to-end."""
# 1. Get OAuth token (mock)
oauth_token = await get_test_oauth_token()
# 2. Register user
response = await client.post(
"/api/v1/auth/register",
json={
"id_token": oauth_token,
"organization_name": "Integration Test Corp"
}
)
assert response.status_code == 201
data = response.json()
assert "license_key" in data
assert "tenant_id" in data
# 3. Verify database state
async with real_database.session() as session:
tenant = await session.execute(
select(Tenant).where(Tenant.id == data["tenant_id"])
)
assert tenant.scalar_one().name == "Integration Test Corp"
Status: Specification Complete ✅ Implementation: Pending (Phase 2) Dependencies: Identity Platform configuration ETA: 1 day
Last Updated: November 23, 2025 Owner: Backend Team Reviewed By: Security Team