Sandbox Only — Not for Production
This API is strictly a learning project and demo sandbox. It is actively monitored — all requests are logged with IP, user agent, and timestamps. Abuse, unauthorized production usage, or excessive automated requests will result in immediate IP bans. By accessing the API, you agree to these terms.
Role
Solo Developer
Runtime
.NET 10 / ASP.NET Core 10
Database
PostgreSQL (Supabase)
Hosting
Fly.io (Docker)
Status
Sandbox — Open for Testing
Note
Password reset endpoints are scaffolded — email service (SendGrid / Resend) not yet wired up
The Problem
Most auth tutorials stop at "hash the password and return a JWT." Real-world auth
needs rate limiting, token refresh, audit trails, role-based access, multiple auth provider
support, and graceful error handling — none of which fits in a blog post. I wanted to build
the full system end-to-end and make it available for anyone to test or plug into their own local projects.
The Approach
Engineered a full-featured authentication API using Clean Architecture with four distinct layers —
Domain, Application, Infrastructure, and Api. The system supports two interchangeable auth providers
(Supabase GoTrue and local BCrypt+JWT) behind a shared IAuthProvider interface, meaning
the entire auth backend can be swapped without touching a single controller. Deployed on Fly.io as a
public sandbox with Swagger docs for easy exploration.
System Architecture
Clean Architecture with strict dependency direction — outer layers depend on inner layers, never the reverse.
The Domain layer has zero dependencies. Dual auth providers implement the same interface so switching
between Supabase and local auth is a single config change.
API Layer — AuthApi.Api
Controllers · Middleware · Auth Handlers
AuthController
UsersController
AdminController
HealthController
ExceptionMiddleware
Rate Limiter
↓ depends on
Application — AuthApi.Application
Interfaces · DTOs · Services
IAuthProvider
IProfileService
IAuditLogger
ITokenService
IUserRepository
All contracts, zero implementations
↓ implements
Infrastructure — AuthApi.Infrastructure
EF Core · Auth Providers · Security
SupabaseAuthProvider
LocalAuthProvider
Repositories
BCrypt Hasher
JWT Service
Domain — AuthApi.Domain
Entities · Enums
AppUser
Profile
AuditLog
RefreshToken
UserRole
Zero dependencies
Auth Flow
Client authenticates via email/password, receives JWT access + refresh tokens.
Protected endpoints validate the Bearer token. Admin endpoints use a separate API key scheme.
Token refresh is handled server-side without re-authentication.
Client
AuthApi
Supabase / Local
Client
access_token + refresh_token
AuthApi
Client
GET /users/me (Bearer token)
AuthApi
API Structure
RESTful API versioned at /api/v1. Auth endpoints are public but rate-limited.
User endpoints require JWT Bearer. Admin endpoints require an API key header.
All errors return a consistent JSON shape with correlation IDs for debugging.
| Method |
Endpoint |
Auth |
Purpose |
| POST |
/api/v1/auth/register |
Public |
Create account, returns JWT tokens |
| POST |
/api/v1/auth/login |
Public |
Authenticate with email/password |
| POST |
/api/v1/auth/refresh |
Public |
Exchange refresh token for new pair |
| POST |
/api/v1/auth/logout |
Bearer |
Revoke current session |
| POST |
/api/v1/auth/forgot-password |
Public |
Initiate password reset · email service not yet integrated |
| POST |
/api/v1/auth/reset-password |
Public |
Reset password with token · pending email service |
| GET |
/api/v1/users/me |
Bearer |
Get current user profile |
| PATCH |
/api/v1/users/me |
Bearer |
Update profile (fullName, avatarUrl) |
| GET |
/api/v1/admin/audit-logs |
API Key |
Paginated audit trail |
| GET |
/api/v1/admin/users |
API Key |
All user profiles (admin only) |
| GET |
/health |
Public |
Liveness probe |
| GET |
/health/dependencies |
Public |
Deep health check (DB + Auth) |
Try It — Sandbox API
The sandbox API is live. Copy any of these curl commands to test the endpoints directly.
curl -X POST https://authapi-clean.fly.dev/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "demo@example.com",
"password": "Test1234!",
"fullName": "Demo User"
}'
Response 200
{
"access_token": "eyJhbGciOi...",
"refresh_token": "v1.MHg3...",
"expires_in": 3600,
"token_type": "bearer",
"user": {
"id": "a1b2c3d4-...",
"email": "demo@example.com",
"role": "authenticated"
}
}
curl -X POST https://authapi-clean.fly.dev/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "demo@example.com",
"password": "Test1234!"
}'
Response 200 — same shape as register
curl -X POST https://authapi-clean.fly.dev/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "<refresh_token>"
}'
Response 200 — new access + refresh pair
curl https://authapi-clean.fly.dev/api/v1/users/me \
-H "Authorization: Bearer <access_token>"
Response 200
{
"id": "a1b2c3d4-...",
"email": "demo@example.com",
"fullName": "Demo User",
"avatarUrl": null,
"role": 0,
"createdAt": "2026-02-21T18:00:00Z",
"updatedAt": "2026-02-21T18:00:00Z"
}
curl https://authapi-clean.fly.dev/health
Response 200
{
"status": "healthy",
"timestamp": "2026-02-21T19:30:00Z",
"version": "1.0.0",
"environment": "Sandbox",
"authProvider": "Supabase"
}
Dual Auth Providers
Both implement IAuthProvider — same interface, different backends.
Switch with a single config value.
Supabase Provider
Currently active on the sandbox
Proxies Supabase GoTrue REST API for auth operations.
Battle-tested JWT infrastructure — no secrets rotation to manage.
Local Provider
Offline / self-hosted
BCrypt (work factor 12) + local JWTs. Full auth flow without
any external service — for dev, testing, or air-gapped deployments.
Security & Rate Limiting
JWT Bearer Auth
User endpoints (/users/*)
Access tokens expire in 15 minutes. Refresh tokens last 7 days,
stored hashed in PostgreSQL.
API Key Auth
Admin endpoints (/admin/*)
Separate scheme via X-Api-Key header.
Keeps admin access decoupled from user auth flow.
Rate Limiting
Per-IP throttling
Auth-sensitive: 5 req / 60s per IP. General: 30 req / 60s.
Built-in ASP.NET Core rate limiter — no external dependencies.
Audit Logging
Every auth action tracked
User ID, IP, user agent, timestamp, success/failure — all logged.
Queryable via admin endpoint with pagination.
Database Design
PostgreSQL with Entity Framework Core 10. Profiles mirror Supabase auth users for local data ownership.
Audit logs track every auth action with IP, user agent, and success/failure for security forensics.
Refresh tokens have expiry tracking for automatic cleanup.
profiles
- id UUID PK
- email VARCHAR UNIQUE
- full_name VARCHAR
- avatar_url VARCHAR NULL
- role ENUM (User/Admin)
app_users
- id UUID PK
- email VARCHAR UNIQUE
- password_hash VARCHAR
- email_confirmed BOOLEAN
audit_logs
- id SERIAL PK
- action VARCHAR
- user_id UUID NULL
- ip_address VARCHAR
- success BOOLEAN
refresh_tokens
- id UUID PK
- user_id FK → app_users
- token_hash VARCHAR
- expires_at TIMESTAMP
- revoked BOOLEAN
Technical Decisions & Tradeoffs
Clean Architecture
Chose: 4-layer strict dependency direction
Domain has zero dependencies. Application defines interfaces only.
Infrastructure implements everything. Api wires it all via DI. This means swapping
PostgreSQL for another DB or Supabase for Firebase touches Infrastructure only —
no controller or service code changes.
Alternative: Vertical slices — faster for small projects, but harder to swap providers.
Dual Auth Providers
Chose: IAuthProvider interface with Supabase + Local implementations
Production uses Supabase GoTrue (battle-tested, managed). Development
and self-hosted deployments use local BCrypt+JWT. Switching is a single config change:
Auth:Provider = "Local". No branching logic in controllers.
Alternative: Supabase only — simpler, but locks you into one vendor.
BCrypt Work Factor 12
Chose: Balanced hashing cost
Work factor 12 takes ~250ms per hash on modern hardware — slow enough to
make brute force impractical, fast enough that login doesn't feel laggy. Lower factors
are vulnerable; higher factors punish legitimate users.
Alternative: Argon2id — more resistant to GPU attacks, but less ecosystem support in .NET.
Docker on Fly.io
Chose: Multi-stage Docker build + Fly.io auto-scaling
Multi-stage build keeps the image lean (no SDK, runtime only).
Fly.io auto-stops when idle and auto-starts on requests — minimal cost for a sandbox.
Forced HTTPS at the edge with no app-level TLS config.
Alternative: Azure App Service — better .NET integration, but higher idle cost.
Error Handling
Every error — validation, auth failure, rate limiting, server error — returns a consistent JSON shape
with a correlationId that maps to server-side logs. Global exception middleware catches
unhandled errors so no stack traces ever leak to clients.
{
"status": 401,
"message": "Invalid credentials.",
"correlationId": "0HNJ..."
}
{
"status": 429,
"message": "Too many requests. Please try again later."
}
What It Does
Register & Login
Token Refresh
Role-Based Access
Rate Limiting
Audit Logging
Health Checks
Password Reset soon
Swagger Docs
What I Learned
Building a full auth system — not a tutorial, but a deployed, documented API — forced me
to think about every edge case: What happens when Supabase is down? How do you prevent user enumeration
on forgot-password? Why does the rate limiter need separate policies for auth vs general endpoints?
Clean Architecture paid off the moment I needed to add the local auth provider — zero changes to
controllers, just a new class implementing the same interface. The dual-provider pattern is something
I'll carry into every future project.