Skip to main content

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 (exp claim)

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