Token Manager System Design
This system implements a resilient Strategy Pattern wrapped in a Facade. It ensures that the application controllers remain agnostic to where tokens are stored (Browser Cookies vs. SQL Database) and how they are secured (Raw JWT vs. Encrypted Blob).
High-Level Architecture
The system uses a strict "Black Box" approach. External packages cannot access internal strategies directly; they must go through the public Manager interface.
Core Design Patterns
1. The Facade (Manager)
The rest of the application interacts only with the Manager interface. This provides a unified API for all token operations.
type Manager interface {
Set(c *gin.Context, t TokenType, value string, opts ...Option) error
Get(c *gin.Context, t TokenType, opts ...Option) (string, error)
DeleteMulti(c *gin.Context, tokens []TokenType, opts ...Option) error
}
2. The Strategy Pattern
Internal logic changes based on the TokenType.
- Access Token: Uses
cookieStrategy. Stores raw JWTs in stateless, secure, HTTP-only cookies. - Refresh Token: Uses
dbStrategy. Encrypts the token and stores it in the database.
3. The Adapter Pattern
To prevent the Token Manager from depending on specific Database Structs (like User), we use Functional Adapters. This bridges the generic (id, string) requirement of the token package to the specific SQL schema of the application.
Token Types & Lifecycle
| Token Type | Storage | Security | Lifecycle |
|---|---|---|---|
TypeAccess | Browser Cookie | Raw Signed JWT | Short-lived (1h). Rotates automatically via middleware. |
TypeRefresh | SQL Database | Encrypted (AES) | Long-lived (90d). Used for WorkOS/App sessions. |
TypeGoogleRefresh | SQL Database | Encrypted (AES) | Long-lived. Used for background Calendar sync. |
TypeAllRefresh | Virtual (SQL) | N/A | Delete Only. Atomic cleanup of all DB sessions. |
Optimized Logout Flow
When a user logs out, we need to clear the browser cookie AND remove the WorkOS session from the database, but we might want to keep the Google Calendar token alive for background jobs (or delete that too).
To avoid multiple database calls, we use DeleteMulti with the virtual TypeAllRefresh strategy.
Integration Example
The system is wired in app.go. This is where the specific SQL queries are injected into the generic manager.
// Wire the specific SQL queries to the generic Manager
workOSAdapter := token.NewRepoAdapter(
// Save
func(ctx context.Context, uid, val string) error {
return userRepo.UpdateColumn(ctx, uid, "workos_token", val)
},
// Get
func(ctx context.Context, uid string) (string, error) {
return userRepo.GetColumn(ctx, uid, "workos_token")
},
// Delete
func(ctx context.Context, uid string) error {
return userRepo.UpdateColumn(ctx, uid, "workos_token", "")
}
)
cfg := token.Config{
MainRefreshRepo: workOSAdapter,
// ... keys and domain settings
}
manager, _ := token.NewManager(cfg)