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 requestUser roles
| Role | Who they are | What they can do |
|---|---|---|
| Patient | End users receiving care | See their own data, book appointments, fill forms, follow treatment plans |
| Specialist | Healthcare providers | See their patients, manage appointments, assign treatment plans |
| Admin | Clinic administrators | Full access to their clinic's data and configuration |
| Customer Support | Support staff | Assist patients across clinics |
| Superadmin | Platform administrators | Manage 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:
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)
CREATE TYPE user_role AS ENUM (
'patient',
'specialist',
'admin',
'customer_support',
'superadmin'
);Database schema
Tables:
users— Internal user records, linked to Clerk viaclerk_user_iduser_organizations— Many-to-many junction for multi-clinic membership
Key columns on users:
clerk_user_id(TEXT, UNIQUE) — Link to Clerk's external IDemail(TEXT, UNIQUE)role(user_role)current_organization_id(BIGINT) — The active clinic contextblocked(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
UserContextin the request context - Rejects with 401 if token is missing, invalid, or user is blocked
2. OrganizationContext middleware
- Reads
current_organization_idfrom 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
| Condition | Response |
|---|---|
Missing Authorization header | 401 Unauthorized |
| Invalid or expired token | 401 Unauthorized |
| User is blocked | 403 Forbidden |
| User has no access to the org | 403 Forbidden |
Clerk webhook events
Clerk notifies the platform when user data changes:
| Event | Action |
|---|---|
user.created | Create internal user record |
user.updated | Sync email/username |
user.deleted | Soft-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
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