Skip to content

Authentication & Users

How people log in, who they are, and what they're allowed to do.


What this enables

  • Patients, specialists, and admins each have their own account with the right level of access
  • Users can belong to multiple clinics and switch between them without logging out
  • Accounts can be blocked without deleting them — access revoked instantly
  • Login, MFA, and password reset are handled by a dedicated, HIPAA-compliant service (Clerk)
  • The platform never stores raw passwords or handles session tokens directly

How it works

When someone opens the app, they log in through Clerk — a dedicated authentication service that handles the security-sensitive parts (passwords, MFA, session tokens). Once logged in, Clerk gives the app a verified token that says who this person is.

The app then looks up that person in its own database to find out which clinic they belong to, what their role is, and what they're allowed to do. From that point on, every database query is automatically filtered to their clinic's data only.

1. User logs in → Clerk
2. Clerk verifies identity → returns a session token
3. User sends token with every request
4. Platform verifies token with Clerk → confirms who they are
5. Platform looks up role and clinic membership
6. Database is scoped to that clinic for this entire request

User roles

RoleWho they areWhat they can do
PatientEnd users receiving careSee their own data, book appointments, fill forms, follow treatment plans
SpecialistHealthcare providersSee their patients, manage appointments, assign treatment plans
AdminClinic administratorsFull access to their clinic's data and configuration
Customer SupportSupport staffAssist patients across clinics
SuperadminPlatform administratorsManage all clinics

Technical Reference

Everything below is intended for developers building on or maintaining the platform.

Architecture

Authentication is split between two systems:

  • Clerk — handles signup, login, MFA, password reset, social auth, session management
  • Platform DB — stores app-specific data: role, clinic memberships, specialist/patient profile links

The link between them is users.clerk_user_id — the Clerk external ID stored in the platform's database.

Organization context & RLS

Multi-clinic isolation is enforced via PostgreSQL session variables set at the start of every request:

sql
set_config('app.current_user_id', '42', true)
set_config('app.current_org_id', '1', true)
set_config('app.current_role', 'specialist', true)

These are transaction-scoped — they apply only to the current request and are cleaned up automatically. All RLS policies on every table reference these variables.

User roles (database type)

sql
CREATE TYPE user_role AS ENUM (
    'patient',
    'specialist',
    'admin',
    'customer_support',
    'superadmin'
);

Database schema

Tables:

  • users — Internal user records, linked to Clerk via clerk_user_id
  • user_organizations — Many-to-many junction for multi-clinic membership

Key columns on users:

  • clerk_user_id (TEXT, UNIQUE) — Link to Clerk's external ID
  • email (TEXT, UNIQUE)
  • role (user_role)
  • current_organization_id (BIGINT) — The active clinic context
  • blocked (BOOLEAN) — When true, all requests are rejected with 403

See schema.sql for the complete schema.

Middleware stack

1. ClerkAuth middleware

  • Extracts Authorization: Bearer <token> header
  • Verifies the token with Clerk
  • Loads the internal user record from the database (one optimized query with specialist/patient joins)
  • Sets UserContext in the request context
  • Rejects with 401 if token is missing, invalid, or user is blocked

2. OrganizationContext middleware

  • Reads current_organization_id from UserContext
  • Acquires a dedicated database connection from the pool
  • Sets RLS session variables on that connection
  • Stores the connection in the request context (all repositories use it for this request)
  • Connection is released when the request completes via defer

3. Repository execution

  • All repositories extract the connection via database.ConnFromContext(ctx)
  • All queries run with RLS session variables already set
  • Tenant isolation is guaranteed by PostgreSQL — not by application code

Fail modes

ConditionResponse
Missing Authorization header401 Unauthorized
Invalid or expired token401 Unauthorized
User is blocked403 Forbidden
User has no access to the org403 Forbidden

Clerk webhook events

Clerk notifies the platform when user data changes:

EventAction
user.createdCreate internal user record
user.updatedSync email/username
user.deletedSoft-delete or block the user

Clerk may retry webhooks. Processed event IDs are tracked in Redis (72-hour TTL) to prevent duplicate processing.

Connection pool consideration

Each request holds a database connection for its entire duration (required for RLS session variables). DB_POOL_MAX must be >= the expected peak concurrent requests. Monitor pool wait time — if it rises above 0 for more than a minute, the pool is undersized.

Usage in handlers

go
func (h *Handler) MyEndpoint(w http.ResponseWriter, r *http.Request) {
    userCtx := middleware.UserFromContext(r.Context())
    // userCtx.ID, userCtx.Role, userCtx.CurrentOrganizationID
}

Implementation files

internal/core/
├── middleware/
│   ├── auth.go              # ClerkAuth middleware
│   └── organization.go      # OrganizationContext middleware + RLS
├── domain/user/
│   ├── model.go             # User, UserRole types
│   ├── repository.go        # DB queries
│   ├── service.go           # Business logic
│   ├── handler.go           # HTTP handlers (/v1/me, webhook)
│   └── errors.go            # Domain errors
└── integration/clerk/
    └── client.go            # Clerk API client