Architecture Guide
This guide explains how GoAuth is built internally. Read this if you want to contribute, build a custom module, or understand the design decisions behind the library.
High-Level Overviewโ
GoAuth is a modular, framework-agnostic authentication library for Go. It is not a standalone service โ it embeds into your application. Everything is built around a plugin system where modules register themselves with a central Auth instance.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Your Application โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ GoAuth Library โ โ
โ โ โ โ
โ โ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโโ โ โ
โ โ โ Core โ โ Session โ โ 2FA โ โ OAuth โ โ โ
โ โ โ Module โ โ Module โ โ Module โ โ Module โ โ โ
โ โ โโโโโโฌโโโโโ โโโโโโฌโโโโโ โโโโโโฌโโโโโโ โโโโโโโฌโโโโโโ โ โ
โ โ โ โ โ โ โ โ
โ โ โโโโโโดโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโดโโโโโโ โ โ
โ โ โ Shared Infrastructure โ โ โ
โ โ โ EventBus ยท SecurityManager ยท Middleware ยท Logger โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Storage Layer โ โ โ
โ โ โ GORM (Postgres ยท MySQL ยท SQLite) โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Package Layoutโ
goauth/
โโโ pkg/ โ Public API (what consumers import)
โ โโโ auth/ โ Auth instance, lifecycle (New โ Use โ Initialize)
โ โโโ config/ โ Config structs, Module interface
โ โโโ models/ โ Data models + repository interfaces
โ โโโ types/ โ Shared types (events, storage, errors, security)
โ โโโ modules/ โ Proxy packages (thin wrappers around internal/)
โ โ โโโ session/
โ โ โโโ invitation/
โ โ โโโ organization/
โ โ โโโ ...
โ โโโ adapters/ โ Framework adapters (stdhttp, gin, etc.)
โ
โโโ internal/ โ Implementation (not importable externally)
โ โโโ modules/ โ Module implementations
โ โ โโโ core/ โ Signup, login, password, verification
โ โ โ โโโ module.go
โ โ โ โโโ handlers/
โ โ โ โโโ services/
โ โ โ โโโ middlewares/
โ โ โ โโโ docs/openapi.yml
โ โ โ โโโ migrations/
โ โ โโโ session/
โ โ โโโ invitation/
โ โ โโโ ...
โ โโโ events/ โ EventBus + worker pool
โ โโโ security/ โ JWT, hashing, encryption
โ โโโ middleware/ โ Global middleware manager
โ
โโโ storage/ โ Storage backends
โ โโโ gorm/ โ GORM implementation
โ โโโ core/
โ โโโ session/
โ โโโ invitation/
โ โโโ organization/
โ โโโ ...
โ
โโโ cli/ โ Developer tools
โ โโโ goauth-gen/ โ Stub generator CLI
โ
โโโ docs/ โ Documentation site (Docusaurus)
Key rule: pkg/ never leaks internal/ types. All public-facing types live in pkg/types/ or pkg/models/. The pkg/modules/ proxy packages re-export internal module constructors with clean signatures.
Three-Phase Lifecycleโ
Every GoAuth application follows this sequence:
graph LR
A["auth.New(config)"] -->|1. Create| B["auth.Use(module)"]
B -->|2. Register| B
B -->|3. Initialize| C["auth.Initialize(ctx)"]
C --> D["Ready to serve"]
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#e8f5e9
style D fill:#f3e5f5
Phase 1: auth.New(config)โ
- Validates config, creates logger, event bus, security manager
- Auto-registers the Core module (always required)
- Returns
*Authโ not yet usable
Phase 2: auth.Use(module)โ
- Registers modules one at a time
- Validates dependencies (e.g., 2FA requires Core)
- Panics if called after Initialize
- Session and Stateless are mutually exclusive
Phase 3: auth.Initialize(ctx)โ
- Runs migrations (if
Migration.Auto: true) - Calls
Init()on every registered module (creates services, handlers) - Registers event hooks
- Collects routes from all modules
- If no auth module registered, defaults to Stateless
Module Contractโ
Every module implements this 8-method interface:
type Module interface {
Name() string // Unique identifier
Init(ctx context.Context, deps ModuleDependencies) error // Initialize with shared deps
Routes() []RouteInfo // HTTP endpoints
Middlewares() []MiddlewareConfig // Middleware definitions
RegisterHooks(events EventBus) error // Event subscriptions
Dependencies() []string // Required module names
OpenAPISpecs() []byte // Embedded OpenAPI YAML
Migrations() ModuleMigrations // Per-dialect SQL migrations
}
All modules receive the same ModuleDependencies struct during Init():
ModuleDependencies
โโโ Storage โ Type-safe storage (Storage.Core(), Storage.Session(), etc.)
โโโ Config โ Global config
โโโ Logger โ Structured logger
โโโ Events โ Event bus for pub/sub
โโโ MiddlewareManager โ Register middleware
โโโ SecurityManager โ JWT, hashing, encryption
โโโ AuthInterceptors โ Hook into login flow (2FA challenges, org claims)
โโโ Options โ Module-specific options
Module Dependency Graphโ
graph TD
Core["Core<br/><small>signup, login, password, verification</small>"]
Session["Session<br/><small>cookie-based auth</small>"]
Stateless["Stateless<br/><small>JWT refresh tokens</small>"]
TwoFactor["Two-Factor<br/><small>TOTP + backup codes</small>"]
OAuth["OAuth<br/><small>Google, GitHub, etc.</small>"]
MagicLink["Magic Link<br/><small>passwordless email</small>"]
Admin["Admin<br/><small>user CRUD</small>"]
Audit["Audit<br/><small>event logging</small>"]
Invitation["Invitation<br/><small>platform invites</small>"]
Organization["Organization<br/><small>multi-tenant</small>"]
Notification["Notification<br/><small>email/SMS delivery</small>"]
Captcha["Captcha<br/><small>bot protection</small>"]
CSRF["CSRF<br/><small>token protection</small>"]
Core --> Session
Core --> Stateless
Core --> TwoFactor
Core --> OAuth
Core --> MagicLink
Core --> Admin
Core --> Audit
Core --> Invitation
Core --> Organization
Core --> Notification
Core --> CSRF
style Core fill:#4CAF50,color:#fff
style Session fill:#FF9800,color:#fff
style Stateless fill:#FF9800,color:#fff
style Notification fill:#9C27B0,color:#fff
linkStyle default stroke:#999
All modules depend on Core (auto-registered). Session and Stateless are mutually exclusive โ registering both panics. Notification is a pure delivery layer with no routes.
Request Flowโ
How an HTTP request flows through GoAuth:
sequenceDiagram
participant Client
participant Mux as HTTP Mux
participant MW as Middleware Chain
participant Handler
participant Service
participant Repo as Repository
participant DB as Database
participant Events as Event Bus
Client->>Mux: POST /auth/signup
Mux->>MW: Route matched โ apply middleware
MW->>MW: RequestID โ Auth (skip for public) โ ...
MW->>Handler: Request with context
Handler->>Handler: Parse & validate DTO
Handler->>Service: service.Signup(ctx, req)
Service->>Repo: repo.Create(ctx, user)
Repo->>DB: INSERT INTO users ...
DB-->>Repo: OK
Repo-->>Service: user
Service->>Events: EmitAsync(EventAfterSignup, userData)
Events-->>Events: Notification hook โ send welcome email
Service-->>Handler: user, nil
Handler-->>Client: 201 Created {user data}
Middleware Priorityโ
Higher priority = runs first:
| Priority | Middleware | Scope |
|---|---|---|
| 100 | CORS (user-provided) | External |
| 90 | RequestID | Global |
| 50 | Auth (JWT validation) | Per-route |
| 45 | Org Auth | Per-route |
| 40 | 2FA Verify | Per-route |
Event Systemโ
GoAuth uses an async event bus for cross-module communication. Modules emit events; other modules subscribe.
graph LR
subgraph Emitters
Core["Core Module"]
Inv["Invitation Module"]
Org["Organization Module"]
end
subgraph EventBus["Event Bus (Worker Pool)"]
Q["Async Queue<br/><small>10 workers, 1000 buffer</small>"]
end
subgraph Subscribers
Notif["Notification<br/><small>sends emails/SMS</small>"]
Audit["Audit<br/><small>logs events</small>"]
Custom["Your Hooks<br/><small>auth.On(...)</small>"]
end
Core -->|EventAfterSignup| Q
Core -->|EventAfterLogin| Q
Inv -->|EventInvitationSent| Q
Org -->|EventOrgInvitationSent| Q
Q --> Notif
Q --> Audit
Q --> Custom
style EventBus fill:#f5f5f5,stroke:#999
style Notif fill:#9C27B0,color:#fff
style Audit fill:#607D8B,color:#fff
Key design: Notification hooks for must-have emails (password reset, magic link, invitations, 2FA) are always registered. They only fire when the corresponding module emits the event. Optional emails (welcome, login alerts) have config flags.
Storage Architectureโ
Storage is type-safe โ each module accesses its own storage interface:
graph TD
S["types.Storage"]
S --> CS["Core()<br/>โ CoreStorage"]
S --> SS["Session()<br/>โ SessionStorage"]
S --> IS["Invitation()<br/>โ InvitationStorage"]
S --> OS["Organization()<br/>โ OrganizationStorage"]
S --> OA["OAuth()<br/>โ OAuthStorage"]
S --> TF["TwoFactorAuth()<br/>โ TwoFactorStorage"]
S --> AL["AuditLog()<br/>โ AuditLogStorage"]
CS --> UR["UserRepository"]
CS --> TR["TokenRepository"]
SS --> SR["SessionRepository"]
IS --> IR["InvitationRepository"]
OS --> OR["OrganizationRepository"]
OS --> MR["MemberRepository"]
OS --> OIR["OrgInvitationRepository"]
OA --> AR["AccountRepository"]
TF --> TFR["TwoFactorRepository"]
TF --> BCR["BackupCodeRepository"]
AL --> ALR["AuditLogRepository"]
style S fill:#1976D2,color:#fff
style CS fill:#42A5F5,color:#fff
style SS fill:#42A5F5,color:#fff
style IS fill:#42A5F5,color:#fff
style OS fill:#42A5F5,color:#fff
Custom storage: Implement the repository interfaces to use any database. Use goauth-gen storage all to scaffold stubs:
go install github.com/bete7512/goauth/cli/goauth-gen@latest
goauth-gen storage all --output ./mystorage --package mystorage
Migration Systemโ
Each module owns its migrations. They are embedded via //go:embed and applied per-dialect:
internal/modules/core/migrations/
โโโ postgres/
โ โโโ 000_init_up.sql
โ โโโ 000_init_down.sql
โ โโโ 001_add_lockout_columns_up.sql
โ โโโ 001_add_lockout_columns_down.sql
โโโ mysql/
โ โโโ ...
โโโ sqlite/
โโโ ...
Migrations are tracked in a goauth_migrations table. Each record stores module_name, version, dialect, and applied_at. During Initialize(), GoAuth compares applied versions against embedded migrations and applies any new ones in order.
Adding a migration: Create a new versioned file (e.g., 002_add_column_up.sql) in each dialect directory. The module's Migrations() method picks it up automatically via //go:embed migrations.
Invitation Flow (Complete)โ
The invitation system supports both platform invitations and org invitations. Both follow the same pattern:
sequenceDiagram
participant Admin as Inviter
participant API as GoAuth API
participant DB as Database
participant EB as Event Bus
participant Email as Email Sender
participant FE as Frontend
participant User as Invited User
Admin->>API: POST /invitations {email, purpose}
API->>DB: Create invitation (token, status=pending)
API->>EB: Emit EventInvitationSent
EB->>Email: Send invitation email
Email->>User: "You're invited! [Accept]"
API-->>Admin: 201 Created
Note over User,FE: User clicks link in email
User->>FE: Opens https://app.com/invite?token=xxx
FE->>FE: Shows accept form
alt New user (no account)
FE->>API: POST /invitations/accept {token, name, password}
API->>DB: Create user (email from invitation)
API->>DB: Mark invitation accepted
API-->>FE: {access_token, refresh_token, is_new_user: true}
else Existing user
FE->>API: POST /invitations/accept {token}
API->>DB: Mark invitation accepted
API-->>FE: {access_token, refresh_token, is_new_user: false}
end
FE-->>User: Logged in, redirected to app
Key design decisions:
- Accept/decline endpoints are public (no auth). The invitation token is the authorization.
- The email link points to a frontend URL (
CallbackURL), not the API. The frontend orchestrates the flow. - New users are created with
email_verified: true(they proved ownership by receiving the email). - Auth tokens are returned immediately โ the user is logged in right after accepting.
Auth Interceptorsโ
Interceptors hook into the login flow to enrich JWT claims or present challenges:
graph LR
Login["Login<br/>Verified"] --> I1["Org Interceptor<br/><small>priority 50</small>"]
I1 -->|add org claims| I2["2FA Interceptor<br/><small>priority 100</small>"]
I2 -->|2FA required?| C{Challenge?}
C -->|No| T["Generate Tokens"]
C -->|Yes| CH["Return Challenge<br/><small>{requires_2fa: true}</small>"]
style Login fill:#4CAF50,color:#fff
style T fill:#2196F3,color:#fff
style CH fill:#FF9800,color:#fff
Interceptors run in priority order (higher first). Each can:
- Add claims to the JWT (e.g.,
active_org_id,org_role) - Return a challenge (e.g., 2FA required) that pauses the login flow
- Add response data (e.g., organization list)
Service Patternโ
All modules follow the same pattern:
// Exported interface โ handlers depend on this
type UserService interface {
Signup(ctx context.Context, req *dto.SignupRequest) (*models.User, *types.GoAuthError)
}
// Unexported struct โ real implementation
type userService struct {
deps config.ModuleDependencies
userRepo models.UserRepository
}
// Constructor returns concrete type satisfying the interface
func NewUserService(deps config.ModuleDependencies, ...) *userService {
return &userService{deps: deps, ...}
}
Error convention: Services return *types.GoAuthError (not error). This carries an HTTP status code and error code for consistent API responses.
Contributing a New Moduleโ
- Create
internal/modules/yourmodule/withmodule.go,config.go,handlers/,services/,docs/openapi.yml - Add module name constant to
pkg/types/module.go - Add route names to
pkg/types/routes.go - If storage needed: add interfaces to
pkg/models/, storage interface topkg/types/storage.go, GORM impl tostorage/gorm/yourmodule/ - If events needed: add event types to
pkg/types/events.go, event data topkg/types/event_data.go - Create proxy package
pkg/modules/yourmodule/ - Add migrations in
migrations/{postgres,mysql,sqlite}/ - Write tests (testify/suite + uber/mock)
- Add OpenAPI spec
Reference implementation: internal/modules/audit/ (simple) or internal/modules/invitation/ (medium complexity).