Skip to main content

Users API


title: Users API audience: Developer difficulty: Intermediate estimated_read_time: 4 min prerequisites:

  • Clerk authentication setup
  • Understanding of user provisioning related_pages:
  • ../api-reference/authentication.md
  • ../getting-started/authentication.md

User provisioning for Clerk authentication.

Overview

The Users API handles just-in-time user provisioning when users sign in with Clerk for the first time.

Base URL: https://env.cat/api/v1/users

Authentication: Public endpoint (called by BFF)

Endpoints

Ensure User

Just-in-time user provisioning. Creates user + personal tenant on first login.

Endpoint:

POST /api/v1/users/ensure

Authorization: None (public, called by BFF)

Note: This endpoint should be secured with BFF signature verification in production (TODO).

Request Body:

{
"clerk_user_id": "user_2abc123xyz",
"email": "user@example.com",
"name": "John Doe"
}

Parameters:

FieldTypeRequiredDescription
clerk_user_idstringYesClerk user ID (format: user_*)
emailstringYesUser's email address
namestringNoUser's display name

Response (200 OK) - User Already Exists:

{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant-uuid",
"role": "owner",
"created": false
}

Response (201 Created) - New User Created:

{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant-uuid",
"role": "owner",
"created": true
}

Response Fields:

FieldTypeDescription
user_idstringUUID of user (existing or newly created)
tenant_idstringUUID of user's personal tenant
rolestringUser's role in tenant (owner)
createdbooleantrue if user was created, false if already existed

Errors:

CodeDescription
400Missing or invalid parameters
500Failed to create user or tenant

Example:

curl -X POST https://env.cat/api/v1/users/ensure \
-H "Content-Type: application/json" \
-d '{
"clerk_user_id": "user_2abc123xyz",
"email": "user@example.com",
"name": "John Doe"
}'

How It Works

First Sign-In Flow

  1. User signs up/in with Clerk at https://env.cat/sign-in
  2. Clerk validates credentials and creates session
  3. Next.js middleware validates Clerk session
  4. BFF calls /api/v1/users/ensure with Clerk user data
  5. Flask API checks if user exists (by clerk_user_id)
  6. If new user:
    • Creates user record with generated username
    • Creates personal tenant (named {username}'s workspace)
    • Creates membership (user is owner of personal tenant)
    • Returns created: true
  7. If existing user:
    • Updates last_login timestamp
    • Returns existing user/tenant info with created: false
  8. User redirected to dashboard

Personal Tenant

Every user automatically gets a personal tenant on first login:

Tenant properties:

  • Name: {username}'s workspace (e.g., john's workspace)
  • Owner: The user (full permissions)
  • Type: Personal (single-user)
  • Isolation: All bundles/keys scoped to this tenant

Username generation:

  • Base username from email: user@example.comuser
  • Limit to 20 characters
  • If username exists, append counter: user1, user2, etc.

Idempotency

This endpoint is idempotent:

  • Calling multiple times with same clerk_user_id returns existing user
  • No duplicate users created
  • Safe to retry on network failures

Security Considerations

Current State (TODO)

Warning: This endpoint is currently public (no BFF signature verification).

Risk: Potential abuse (creating fake users)

Mitigation (current):

  • Rate limiting recommended
  • Clerk user IDs must start with user_ (validation)
  • Requires valid email format

Production Hardening (TODO)

Add BFF signature verification:

@bp.route('/ensure', methods=['POST'])
@verify_bff_signature # TODO: Add this decorator
def ensure_user():
# ...

Why needed:

  • Ensures only BFF can call this endpoint
  • Prevents direct API abuse
  • Maintains security boundary

Testing

Local Development

# Test user provisioning
curl -X POST http://localhost:8080/api/v1/users/ensure \
-H "Content-Type: application/json" \
-d '{
"clerk_user_id": "user_2test123",
"email": "test@example.com",
"name": "Test User"
}'

# Response (first call):
# {"user_id":"...","tenant_id":"...","role":"owner","created":true}

# Response (second call - idempotent):
# {"user_id":"...","tenant_id":"...","role":"owner","created":false}

Production

Do not call this endpoint directly in production. It's intended to be called by the BFF layer during Clerk authentication flow.

Database Schema

User Table

CREATE TABLE users (
id TEXT PRIMARY KEY,
clerk_user_id TEXT UNIQUE NOT NULL,
username TEXT UNIQUE NOT NULL,
email TEXT NOT NULL,
auth_provider TEXT DEFAULT 'clerk',
password_hash TEXT, -- NULL for Clerk users
is_active BOOLEAN DEFAULT 1,
failed_login_attempts INTEGER DEFAULT 0,
locked_until TIMESTAMP,
last_login TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Tenant Table

CREATE TABLE tenants (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Membership Table

CREATE TABLE memberships (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
tenant_id TEXT NOT NULL REFERENCES tenants(id),
role TEXT NOT NULL, -- 'owner' or 'member'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, tenant_id)
);

See Also