Skip to main content

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:

PriorityMiddlewareScope
100CORS (user-provided)External
90RequestIDGlobal
50Auth (JWT validation)Per-route
45Org AuthPer-route
402FA VerifyPer-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โ€‹

  1. Create internal/modules/yourmodule/ with module.go, config.go, handlers/, services/, docs/openapi.yml
  2. Add module name constant to pkg/types/module.go
  3. Add route names to pkg/types/routes.go
  4. If storage needed: add interfaces to pkg/models/, storage interface to pkg/types/storage.go, GORM impl to storage/gorm/yourmodule/
  5. If events needed: add event types to pkg/types/events.go, event data to pkg/types/event_data.go
  6. Create proxy package pkg/modules/yourmodule/
  7. Add migrations in migrations/{postgres,mysql,sqlite}/
  8. Write tests (testify/suite + uber/mock)
  9. Add OpenAPI spec

Reference implementation: internal/modules/audit/ (simple) or internal/modules/invitation/ (medium complexity).