# Velocity Documentation > Velocity is a full-stack Go web framework with unified API, driver-based architecture, and zero configuration lock-in. Build faster, ship sooner. Source repository: https://github.com/velocitykode/velocity Live docs: https://vel.build/docs/ Lean index: https://vel.build/llms.txt ================================================================================ # Getting Started Source: https://vel.build/docs/getting-started/getting-started/ Section: Getting Started Summary: Install Velocity CLI, create your first Go web application, and run the development server with hot reload. ## Installation ### Prerequisites - Go 1.26 or higher - Node.js 18+ (for frontend assets) - Git ### Install the Velocity CLI ```bash brew tap velocitykode/tap brew install velocity ``` ```bash go install github.com/velocitykode/velocity-cli@latest ``` Verify the installation: ```bash velocity --version ``` ## Creating Your First Project Create a new Velocity application: ```bash velocity new myapp ``` This creates a new project and automatically starts the development servers. Your application will be available at: Building an API without a frontend? Use `velocity new myapi --api` to create an API-only project. See the [Installer Commands](/docs/cli/installer/) page for the full `velocity new` flag reference. - **Go server**: http://localhost:4000 - **Vite dev server**: http://localhost:5173 ### Project Structure ``` myapp/ ├── internal/ │ ├── app/ # Bootstrap: CSRF, view engine, middleware stacks │ ├── handlers/ # HTTP handlers │ ├── middleware/ # Custom middleware │ └── models/ # Database models ├── config/ # Configuration files ├── database/ │ └── migrations/ # Database migrations ├── public/ # Static assets ├── resources/ │ ├── js/ # JavaScript/React files │ ├── css/ # Stylesheets │ └── views/ # Root HTML template (Inertia) ├── routes/ # Route definitions ├── storage/ │ └── logs/ # Application logs ├── .env # Environment variables ├── go.mod # Go module file ├── package.json # Node.js dependencies (full-stack only) ├── vite.config.ts # Vite configuration └── main.go # Application entry point ``` ## Quick Start Example Here's what the generated `main.go` looks like: ```go package main import ( "log" "os" "myapp/internal/app" "myapp/routes" "github.com/velocitykode/velocity" // Blank import so each migration file's init() runs - otherwise // `vel migrate` finds nothing. _ "myapp/database/migrations" ) func main() { v, err := velocity.New() if err != nil { log.Fatal(err) } chain := v. Providers(app.Configure). // auth, CSRF, view engine setup Middleware(app.Middleware). // global / web / API stacks Routes(routes.Register). // your route definitions Events(app.Events(v.Log)) // your event listeners // With CLI args - dispatch a `vel ...` command. if len(os.Args) > 1 { if err := chain.Run(); err != nil { log.Fatal(err) } return } // Otherwise - start the HTTP server. if err := chain.Serve(); err != nil { log.Fatal(err) } } ``` The four callbacks (`app.Configure`, `app.Middleware`, `app.Events`, `routes.Register`) live in your own `app` and `routes` packages - `velocity new` scaffolds them for you. See [Routing](/docs/core/routing) and [Middleware](/docs/core/middleware) for the shapes. Define routes in `routes/web.go`: ```go package routes import ( "github.com/velocitykode/velocity" "github.com/velocitykode/velocity/router" ) func Register(r *velocity.Routing) { r.Web(func(web router.Router) { web.Get("/", func(ctx *router.Context) error { ctx.Response.Write([]byte("Welcome to Velocity!")) return nil }) }) } ``` See [Routing](/docs/core/routing) for groups, middleware stacks, API routes, and the full reference. ## Development Server Start the development server with hot reload: ```bash vel serve ``` The development server includes: - **Hot Reload**: Automatically restarts when Go files change - **Error Pages**: Detailed error messages with stack traces - **Request Logging**: Logs all requests and responses ### Serve Options ```bash # Custom port vel serve --port 8080 # Disable hot reload vel serve --no-watch # Specify environment vel serve --env production ``` ## Building for Production Create an optimized production build: ```bash vel build ``` This produces a single binary with: - Stripped debug symbols for smaller size - Static linking for portability - Ready for deployment ### Build Options ```bash # Custom output path vel build --output ./bin/myapp # Cross-compile for Linux vel build --os linux --arch amd64 # Build with Go build tags vel build --tags prod ``` ## Configuration ### Environment Variables Velocity uses `.env` files for configuration. The installer writes a full `.env` with random keys; the most commonly edited values: ```bash APP_NAME=MyApp APP_ENV=development APP_URL=http://localhost:4000 APP_PORT=4000 # Logging LOG_DRIVER=console # console, file LOG_LEVEL=info # Encryption / signing - installer populates these at scaffold time APP_KEY= QUEUE_SIGNING_KEY= JWT_SECRET= CRYPTO_CIPHER=AES-256-CBC # Database DB_CONNECTION=sqlite # postgres, mysql, sqlite DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=database.sqlite DB_USERNAME= DB_PASSWORD= # Cache CACHE_DRIVER=memory # redis, memory ``` `APP_KEY` doubles as the crypto key. Set `CRYPTO_KEY` explicitly only if you want a dedicated encryption key separate from the app key. ### Regenerating the application key ```bash vel key:generate ``` This generates a fresh 32-byte base64 key and writes it to `.env` - useful if you need to rotate the key or the installer didn't run `key:generate` for you. ## Next Steps - [CLI Reference](/docs/cli/) - Full CLI command documentation - [Routing](/docs/core/routing/) - Learn about routing and middleware - [Database](/docs/database/) - Set up database connections and models - [Frontend](/docs/frontend/) - Configure Vite and Inertia.js ================================================================================ # Installation Source: https://vel.build/docs/cli/installation/ Section: CLI Summary: Install the Velocity CLI on macOS using Homebrew. Create and manage Go web applications with the velocity command line tool. Install the Velocity CLI to create and manage Velocity projects. ## Requirements - **Go 1.26 or higher** - Required for building projects - **Node.js 18+** - Required for frontend asset compilation (Vite) - **Git** - Required for project initialization ## Install via Homebrew The recommended way to install Velocity on macOS: ```bash brew tap velocitykode/tap brew install velocity ``` ## Verify Installation Check that the CLI is installed correctly: ```bash velocity --version ``` You should see output like: ``` velocity version 0.4.0 ``` ## Understanding the CLI Architecture Velocity uses two CLI tools: | Tool | Install Method | Purpose | |------|---------------|---------| | `velocity` | Homebrew (global) | Create projects, manage config | | `vel` | Built from source (per-project) | Run dev server, migrations, generators | When you run `velocity new myapp`, it: 1. Scaffolds a new project 2. Installs dependencies 3. Builds the `./vel` binary from your project source 4. Starts development servers ## Using vel in Projects After creating a project, use `./vel` for development commands: ```bash cd myapp ./vel serve # Start dev server ./vel migrate # Run migrations ``` ### Shell Function (Recommended) Run this once to use `vel` instead of `./vel`: ```bash grep -q "vel()" ~/.zshrc || echo 'vel() { [ -x ./vel ] && ./vel "$@" || echo "vel: not found"; }' >> ~/.zshrc && source ~/.zshrc ``` Now you can simply run: ```bash vel serve vel migrate ``` ## Getting Help View available commands: ```bash velocity --help ./vel --help ``` Get help for a specific command: ```bash velocity new --help ./vel serve --help ``` ## Updating ### Update Velocity Installer ```bash brew upgrade velocity ``` Or use the built-in self-update: ```bash velocity self-update ``` ### Rebuild vel The `vel` binary is rebuilt automatically when source files change. To manually rebuild: ```bash go build -o vel ./cmd/vel ``` ## Uninstalling ### Remove Velocity Installer ```bash brew uninstall velocity brew untap velocitykode/tap ``` ### Remove vel The `vel` binary is project-local and gitignored. Simply delete your project directory. ================================================================================ # vel Commands Source: https://vel.build/docs/cli/commands/ Section: CLI Summary: Complete reference for the per-project `vel` CLI - serve, build, migrations, queues, code generation, maintenance, and keys. `vel` is the per-project binary. It's created in your project root when you scaffold an app with `velocity new`. Run `./vel ` - or alias `vel` to `./vel` in your shell - from the project directory. For the installer CLI (`velocity new`, `velocity self-update`, etc.), see [Installer Commands](/docs/cli/installer/). ## Server ### vel serve Start the development server with live reload. ```bash vel serve [flags] ``` | Flag | Short | Default | Description | | ----------- | ----- | ------------- | ----------------------------------------- | | `--port` | `-p` | `4000` | HTTP port | | `--env` | `-e` | `development` | Environment name (sets `APP_ENV`) | | `--no-watch`| | off | Disable file-watching / auto-rebuild | | `--tags` | | (none) | Build tags passed to `go build` | ```bash vel serve vel serve --port 3000 vel serve --env staging --no-watch vel serve --tags="integration" ``` On start: 1. Vite dev server launches on `:5173` (full-stack projects only). 2. The Go app compiles to `.vel/tmp/server`. 3. `.go` files are watched; a change rebuilds and restarts. ### vel build Compile a production binary. ```bash vel build [flags] ``` | Flag | Short | Default | Description | | ----------- | ----- | ------------- | ------------------------------- | | `--output` | `-o` | `./dist/app` | Output path | | `--os` | | (host) | Target `GOOS` | | `--arch` | | (host) | Target `GOARCH` | | `--tags` | | (none) | Go build tags | ```bash vel build vel build --output ./bin/myapp vel build --os linux --arch amd64 ``` ## Database ### vel migrate Run all pending migrations. ```bash vel migrate [--pretend] ``` `--pretend` prints the SQL that would run without executing it - useful for reviewing migration output before committing. ### vel migrate:fresh Drop all tables, then run every migration from scratch. ```bash vel migrate:fresh ``` Destructive - deletes all data. Development / testing only. ### vel migrate:rollback Roll back the most recent batch of migrations. ```bash vel migrate:rollback [--step N] ``` | Flag | Short | Default | Description | | -------- | ----- | ------- | -------------------------------- | | `--step` | `-s` | `1` | Number of batches to roll back | ### vel migrate:status Show which migrations have run. ```bash vel migrate:status ``` ### vel db:wipe Drop every table in the current database without running migrations. ```bash vel db:wipe ``` Destructive. No confirmation prompt. Use only when you know the database is disposable. ## Queue and Scheduler ### vel queue:work Start a worker that processes queued jobs. ```bash vel queue:work [--queue NAME] [--tries N] [--timeout S] ``` | Flag | Short | Default | Description | | ----------- | ----- | ------------ | ------------------------------------------ | | `--queue` | `-q` | `default` | Queue to consume from | | `--tries` | | (driver) | Max attempts per job before marking failed | | `--timeout` | | (driver) | Per-job timeout in seconds | ```bash vel queue:work vel queue:work --queue emails --tries 3 --timeout 60 ``` ### vel schedule:work Run the scheduler loop - picks up scheduled jobs defined via `v.Schedule(...)` and dispatches them when due. ```bash vel schedule:work ``` Typically run under a process supervisor (systemd, Docker, etc.) rather than manually. ## Cache ### vel cache:clear Flush the configured cache store. ```bash vel cache:clear ``` ## Maintenance Mode ### vel down Put the app into maintenance mode. All requests return 503 except those bearing the bypass secret. ```bash vel down [--secret TOKEN] [--retry N] ``` | Flag | Default | Description | | ---------- | ------- | ------------------------------------------------------ | | `--secret` | (none) | Token clients can use at `?__maintenance=TOKEN` to bypass | | `--retry` | (none) | Value for the `Retry-After` response header (seconds) | ```bash vel down --secret "abc123" --retry 60 ``` ### vel up Exit maintenance mode. ```bash vel up ``` ## Keys ### vel key:generate Generate a fresh 32-byte encryption key and write it to `.env` under `CRYPTO_KEY` (falls back to `APP_KEY` if that's what the project uses). ```bash vel key:generate ``` ## Routes ### vel route:list Print every registered route with method, path, name, and middleware. ```bash vel route:list ``` Rebuilds the bootstrap lifecycle internally before printing - the output always reflects the current `v.Routes(...)` definition. ## Code Generation All `make:*` commands scaffold a file into the conventional location for that type. Names are converted to the right case for the artifact (snake_case for migration files, PascalCase for types). ### vel make:handler ```bash vel make:handler [--resource] [--api] ``` | Flag | Short | Default | Description | | ------------ | ----- | ------- | ------------------------------------------------ | | `--resource` | `-r` | off | Scaffold CRUD methods (Index/Show/Store/Update/Destroy) | | `--api` | | off | JSON responses instead of view rendering | Output: `internal/handlers/.go`. ```bash vel make:handler User vel make:handler Post --resource vel make:handler Admin/Dashboard vel make:handler Product --api --resource ``` ### vel make:model ```bash vel make:model [--uuid] [--soft-deletes] [--migration] ``` | Flag | Short | Default | Description | | ----------------- | ----- | ------- | ------------------------------------ | | `--uuid` | | off | Use UUID primary key | | `--soft-deletes` | | off | Add deleted_at column and scope | | `--migration` | `-m` | off | Also scaffold the migration | Output: `internal/models/.go`. ### vel make:migration ```bash vel make:migration [--create TABLE] [--table TABLE] [--uuid] [--soft-deletes] ``` | Flag | Accepts | Description | | ---------------- | ------------------- | --------------------------------------------------- | | `--create` | `=VALUE` or space | Generate a "create" migration for the given table | | `--table` | `=VALUE` or space | Generate an "alter" migration for the given table | | `--uuid` | flag | Use UUID primary key in the create template | | `--soft-deletes` | flag | Include deleted_at in the create template | Output: `database/migrations/_.go`. ```bash vel make:migration create_posts --create=posts vel make:migration add_slug_to_posts --table=posts ``` ### Other make commands All take a name argument and scaffold a file into the conventional directory. | Command | Output path | | ---------------------- | ------------------------------------ | | `vel make:middleware` | `internal/middleware/.go` | | `vel make:event` | `internal/events/.go` | | `vel make:listener` | `internal/listeners/.go` | | `vel make:job` | `internal/jobs/.go` | | `vel make:mail` | `internal/mail/.go` | | `vel make:notification`| `internal/notifications/.go` | | `vel make:resource` | `internal/resources/.go` | | `vel make:policy` | `internal/policies/.go` | | `vel make:provider` | `internal/providers/.go` | | `vel make:command` | `internal/commands/.go` | ```bash vel make:middleware RateLimit vel make:listener SendWelcomeEmail vel make:policy PostPolicy ``` ### vel make:grpc:service ```bash vel make:grpc:service ``` Scaffolds a gRPC service end-to-end in one call: - `api/proto//v1/.proto` - empty service block - `api/proto/buf.yaml` + `api/proto/buf.gen.yaml` (first run only) - `internal/grpc/services/.go` - `*Service` impl with `UnimplementedXXXServer` embed - `internal/providers/grpc_provider.go` - created on first call, then **injected at** `// vel:grpc:imports` and `// vel:grpc:services` markers on every subsequent call Name normalisation: `vel make:grpc:service Foo`, `FooService`, `foo`, and `fooService` all produce `FooService` with proto package `foo.v1` and Go alias `foov1`. The proto file uses `option go_package = "/api/gen/go//v1;v1"` derived from the host project's `go.mod`. `buf.yaml` / `buf.gen.yaml` are written **before** the proto file, so a config-write failure leaves no partial scaffold on disk. The generated `GRPCProvider` does **not** hard-code `WithReflection(true)`; the framework default (toggled by `GRPC_REFLECTION` env) reaches the file so the generated app boots cleanly in production. If `internal/providers/grpc_provider.go` already exists **without** the marker comments (legacy hand-written provider), the command prints a manual wire snippet instead of mutating user code. ```bash vel make:grpc:service Foo vel make:grpc:service ChatService ``` After scaffolding, register `providers.GRPCProvider{}` in `internal/app/bootstrap.go` (printed as a hint on first run). ### vel make:grpc:rpc ```bash vel make:grpc:rpc [--stream | --client-stream | --bidi] ``` Appends a new rpc to an existing service's `.proto` and a matching method stub on the Go impl. The service must already exist; run `vel make:grpc:service ` first. | Flag | Aliases | RPC shape produced | | ----------------- | ------------------- | ------------------------------------------------- | | _(none)_ | | Unary: `rpc X(XRequest) returns (XResponse)` | | `--stream` | `--server-stream` | Server-streaming: `returns (stream XResponse)` | | `--client-stream` | | Client-streaming: `(stream XRequest) returns (X)` | | `--bidi` | `--bidirectional` | Bidi: `(stream XRequest) returns (stream X)` | Only one streaming flag may be set per invocation; combining them errors out. The proto scanner walks the file with brace counting that respects `//` line comments, `/* block */` comments, and `"..."` string literals at every position (header keyword, between keyword and name, between name and `{`, and inside the body). That means rpc-with-options blocks (grpc-gateway HTTP annotations) and commented-out draft headers do not corrupt insertion. On the Go side, the generated method signature matches the RPC shape: | Shape | Signature | | ------------- | ------------------------------------------------------------------------- | | Unary | `func (s *Foo) X(ctx context.Context, req *foov1.XRequest) (*foov1.XResponse, error)` | | Server stream | `func (s *Foo) X(req *foov1.XRequest, stream foov1.Foo_XServer) error` | | Client stream | `func (s *Foo) X(stream foov1.Foo_XServer) error` | | Bidi | `func (s *Foo) X(stream foov1.Foo_XServer) error` | `context` is added to the impl's imports for unary only; streaming variants pull ctx from `stream.Context()` and do not need the import. Idempotent: re-running with the same ` ` pair detects the existing rpc and skips. ```bash vel make:grpc:rpc Foo Hello vel make:grpc:rpc Foo Tail --stream vel make:grpc:rpc Foo Upload --client-stream vel make:grpc:rpc Foo Chat --bidi ``` ### vel make:grpc:gen ```bash vel make:grpc:gen ``` Runs `buf generate` inside `api/proto`. Streams buf's stdout and stderr to your terminal so plugin errors are visible in real time. Fails with a clear message when: - `api/proto/` does not exist (run `make:grpc:service` first) - `buf` is not on `PATH` (links to install docs) - `buf generate` exits non-zero ```bash vel make:grpc:gen # cd api/proto && buf generate # Generated Go code in api/gen/go/ ``` ## Help ```bash vel help vel --help vel -h ``` Prints a grouped list of every command. ================================================================================ # Configuration Source: https://vel.build/docs/cli/configuration/ Section: CLI Summary: Configure Velocity CLI defaults for database, cache, and queue drivers. Set project scaffolding preferences for faster development. The Velocity CLI stores global configuration preferences that apply when creating new projects. ## Configuration File Configuration is stored in `~/.vel/config.yaml`. This file is created automatically when you first set a configuration value. ## Available Settings | Setting | Description | Valid Values | |---------|-------------|--------------| | `default.database` | Default database driver for new projects | `postgres`, `mysql`, `sqlite` | | `default.cache` | Default cache driver | `redis`, `memory` | | `default.queue` | Default queue driver | `redis`, `database`, `sync` | | `default.auth` | Include authentication by default | `true`, `false` | | `default.api` | Create API-only projects by default | `true`, `false` | ## Setting Defaults Configure your preferred defaults so you don't have to specify them every time: ```bash # Set PostgreSQL as default database velocity config set default.database postgres # Set Redis as default cache velocity config set default.cache redis # Always include authentication velocity config set default.auth true ``` Now when you create a new project, these defaults are used: ```bash # Uses postgres + redis + auth without specifying flags velocity new myapp ``` ## Viewing Configuration List all current settings: ```bash velocity config list ``` Get a specific value: ```bash velocity config get default.database ``` ## Resetting Configuration Clear all settings and return to defaults: ```bash velocity config reset ``` ## Priority Order When creating a project, values are resolved in this order: 1. **Command-line flags** (highest priority) 2. **Configuration file** (`~/.vel/config.yaml`) 3. **Built-in defaults** (lowest priority) **Example:** ```bash # Config has default.database = postgres # But this command uses mysql (flag overrides config) velocity new myapp --database mysql ``` ## Example Configuration File ```yaml # ~/.vel/config.yaml defaults: database: postgres cache: redis queue: redis auth: true api: false ``` ================================================================================ # velocity Commands Source: https://vel.build/docs/cli/installer/ Section: CLI Summary: Reference for the global `velocity` installer - scaffold new projects, manage CLI defaults, and keep the installer up to date. `velocity` is the global installer CLI. You install it once ([installation](/docs/cli/installation)) and use it to create new projects, configure defaults, and update itself. Per-project commands (`serve`, `build`, `migrate`, `make:*`) live on the `vel` binary inside each project - see [vel commands](/docs/cli/commands). ## velocity new Create a new Velocity project. ```bash velocity new [flags] ``` | Flag | Default | Description | | ------------- | --------- | --------------------------------------------------------- | | `--database` | `sqlite` | Database driver: `postgres`, `mysql`, `sqlite` | | `--cache` | `memory` | Cache driver: `redis`, `memory` | | `--api` | `false` | API-only project (no frontend) | | `--ssr` | `false` | Enable Inertia SSR (sets `INERTIA_SSR_ENABLED=true`, wires Vite SSR) | ```bash velocity new myapp velocity new myapp --database postgres --cache redis velocity new myapi --api --database postgres velocity new myapp --ssr ``` ### What `new` does 1. Fails fast if the destination path already exists. 2. Clones the appropriate starter template (`velocity-template-react` or `velocity-template-api`). 3. Rewrites the Go module name to match the project. 4. Installs Go dependencies - plus npm dependencies on full-stack projects. 5. Runs `vel key:generate` to seed `.env`. 6. Runs the initial migrations against the configured database. 7. Builds the project's `vel` binary. 8. Launches the dev server - Go on `:4000`, Vite on `:5173` for full-stack. ### API vs full-stack The `--api` flag picks a different starter: | Aspect | Full-stack (default) | `--api` | | ---------------- | --------------------------------- | -------------------------------- | | Frontend | Vite + Inertia + React | None | | CSRF | Enabled | Disabled (stateless) | | Auth guard | `session` | `api` (token-based) | | Error responses | Inertia-rendered error pages | JSON 401 / 403 / 404 | | Starter routes | `/`, `/login`, `/register`, ... | `/api/health`, `/api/users`, ... | You can't flip between modes after scaffolding - choose up front. ## velocity config Manage global CLI defaults. Defaults are loaded on every run of `velocity new` and pre-fill the interactive prompts. ```bash velocity config set velocity config get velocity config list velocity config reset ``` ### Keys | Key | Accepted values | Default | | ------------------- | -------------------------------- | --------- | | `default.database` | `postgres`, `mysql`, `sqlite` | `sqlite` | | `default.cache` | `redis`, `memory` | `memory` | | `default.queue` | `redis`, `database` | `database`| | `default.auth` | `true`, `false` | `true` | | `default.api` | `true`, `false` | `false` | ### Examples ```bash velocity config set default.database postgres velocity config set default.cache redis velocity config set default.api true velocity config get default.database # → postgres velocity config list # all values velocity config reset # wipe the config file ``` Configuration is stored at `~/.vel/config.yaml` (platform-conventional location on other systems). ## velocity self-update Fetch and install the latest installer release. ```bash velocity self-update ``` Updates the binary in place. The running process exits after the new binary is downloaded - rerun any command to pick it up. ## velocity --version Print the installer version. ```bash velocity --version ``` ## Go version check The installer verifies that Go is installed and meets the minimum version (1.26 or higher) on every run. When the check fails you'll see one of: ``` Go is not installed or not in PATH. Velocity requires Go 1.26 or higher. ``` ``` Go version go1.24.2 is not supported. Velocity requires Go 1.26 or higher. ``` Install or upgrade Go (`brew upgrade go` or [go.dev/dl](https://go.dev/dl)) and run the command again. ================================================================================ # Configuration Source: https://vel.build/docs/core/config/ Section: Core Framework Summary: Manage application configuration with environment variables and structured config files in Velocity. Velocity provides a simple yet powerful configuration system that reads from environment variables and provides structured configuration for framework packages. ## Quick Start **Environment-Based**: Velocity uses environment variables for all configuration, following the twelve-factor app methodology. ```go import "github.com/velocitykode/velocity/config" func main() { // Get environment variable with fallback appName := config.Get("APP_NAME", "Velocity") appEnv := config.Get("APP_ENV", "development") fmt.Printf("Running %s in %s mode\n", appName, appEnv) } ``` ```env # .env file APP_NAME=MyApplication APP_ENV=production APP_DEBUG=false APP_URL=https://example.com # Database DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE=myapp DB_USERNAME=root DB_PASSWORD=secret # Cache CACHE_DRIVER=redis REDIS_HOST=localhost REDIS_PORT=6379 ``` ```go import "github.com/velocitykode/velocity/config" func main() { // String with fallback appName := config.Env("APP_NAME", "Velocity") // Integer with fallback port := config.EnvInt("APP_PORT", 4000) // Boolean with fallback debug := config.EnvBool("APP_DEBUG", false) fmt.Printf("%s running on port %d (debug: %v)\n", appName, port, debug) } ``` ## Configuration Files ### .env File Create a `.env` file in your project root: ```env # Application APP_NAME=Velocity APP_ENV=production APP_DEBUG=false APP_URL=https://example.com APP_PORT=4000 # Crypto (for session encryption) CRYPTO_KEY=base64:your-32-byte-base64-encoded-key CRYPTO_CIPHER=AES-256-CBC # Database DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE=velocity DB_USERNAME=root DB_PASSWORD= # Cache CACHE_DRIVER=redis CACHE_PREFIX=velocity_cache REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD= REDIS_DATABASE=0 # Logging LOG_DRIVER=file LOG_PATH=./storage/logs LOG_LEVEL=debug # Queue QUEUE_DRIVER=memory QUEUE_REDIS_HOST=localhost QUEUE_REDIS_PORT=6379 # Mail MAIL_DRIVER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME= MAIL_PASSWORD= MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=noreply@example.com MAIL_FROM_NAME="${APP_NAME}" # Session SESSION_DRIVER=cookie SESSION_LIFETIME=120 SESSION_SECURE=false SESSION_HTTP_ONLY=true SESSION_SAME_SITE=lax # WebSocket WEBSOCKET_HOST=0.0.0.0 WEBSOCKET_PORT=6001 WEBSOCKET_PATH=/ws ``` ### Loading Environment Variables Environment variables are loaded automatically by the framework, but you can also load them manually: ```go import ( "github.com/joho/godotenv" "log" ) func init() { // Load .env file if err := godotenv.Load(); err != nil { log.Println("No .env file found") } } ``` ## API Reference ### Get Retrieve an environment variable with a fallback: ```go import "github.com/velocitykode/velocity/config" // Get with fallback appName := config.Get("APP_NAME", "DefaultApp") // Get without fallback (returns empty string if not set) apiKey := config.Get("API_KEY", "") ``` ### Env Alias for `Get` - retrieve string environment variable: ```go // Get string value appEnv := config.Env("APP_ENV", "development") appURL := config.Env("APP_URL", "http://localhost:4000") ``` ### EnvInt Retrieve an integer environment variable: ```go // Get integer value port := config.EnvInt("APP_PORT", 4000) maxConnections := config.EnvInt("MAX_CONNECTIONS", 100) // Returns default value if not set or invalid timeout := config.EnvInt("TIMEOUT", 30) ``` ### EnvBool Retrieve a boolean environment variable: ```go // Get boolean value debug := config.EnvBool("APP_DEBUG", false) enableCache := config.EnvBool("ENABLE_CACHE", true) // Accepts: true, false, 1, 0, yes, no (case-insensitive) ``` ## Structured Configuration ### Logging Configuration The config package provides structured configuration for logging: ```go import "github.com/velocitykode/velocity/config" // Get logging configuration loggingConfig := config.GetLoggingConfig() // Access default channel defaultChannel := loggingConfig.Default // "stack" // Get specific channel config if channelConfig, exists := loggingConfig.GetChannel("daily"); exists { fmt.Printf("Driver: %s\n", channelConfig.Driver) fmt.Printf("Path: %s\n", channelConfig.Path) fmt.Printf("Level: %s\n", channelConfig.Level) } // Get default channel config if defaultConfig, exists := loggingConfig.GetDefaultChannel(); exists { fmt.Printf("Default driver: %s\n", defaultConfig.Driver) } ``` ### Channel Configuration Configure individual log channels: ```go type ChannelConfig struct { Driver string // Driver name (file, console, syslog, null) Level string // Log level (debug, info, warn, error) Path string // File path (for file driver) MaxSize int // Max size in MB MaxAge int // Max age in days MaxBackups int // Number of old files to keep Format string // Format (json, text) Options map[string]interface{} // Driver-specific options } ``` ## Environment-Specific Configuration ### Development Environment ```env APP_ENV=development APP_DEBUG=true APP_URL=http://localhost:4000 LOG_LEVEL=debug LOG_DRIVER=console CACHE_DRIVER=memory QUEUE_DRIVER=memory DB_HOST=localhost DB_DATABASE=myapp_dev ``` ### Production Environment ```env APP_ENV=production APP_DEBUG=false APP_URL=https://example.com LOG_LEVEL=info LOG_DRIVER=file CACHE_DRIVER=redis QUEUE_DRIVER=redis DB_HOST=db.example.com DB_DATABASE=myapp_prod ``` ### Testing Environment ```env APP_ENV=testing APP_DEBUG=true LOG_LEVEL=error LOG_DRIVER=null CACHE_DRIVER=memory QUEUE_DRIVER=memory DB_DATABASE=myapp_test ``` ## Configuration Patterns ### Application Bootstrap Centralize configuration loading in your app initialization: ```go package app import ( "log" "os" "github.com/joho/godotenv" "github.com/velocitykode/velocity/config" ) type AppConfig struct { Name string Environment string Debug bool URL string Port int } func LoadConfig() *AppConfig { // Load .env file if err := godotenv.Load(); err != nil { log.Println("No .env file found, using environment variables") } return &AppConfig{ Name: config.Get("APP_NAME", "Velocity"), Environment: config.Get("APP_ENV", "development"), Debug: config.EnvBool("APP_DEBUG", false), URL: config.Get("APP_URL", "http://localhost:4000"), Port: config.EnvInt("APP_PORT", 4000), } } ``` ### Database Configuration ```go type DatabaseConfig struct { Connection string Host string Port int Database string Username string Password string } func GetDatabaseConfig() *DatabaseConfig { return &DatabaseConfig{ Connection: config.Get("DB_CONNECTION", "mysql"), Host: config.Get("DB_HOST", "localhost"), Port: config.EnvInt("DB_PORT", 3306), Database: config.Get("DB_DATABASE", "velocity"), Username: config.Get("DB_USERNAME", "root"), Password: config.Get("DB_PASSWORD", ""), } } ``` ### Cache Configuration ```go type CacheConfig struct { Driver string Prefix string Host string Port int Password string Database int } func GetCacheConfig() *CacheConfig { return &CacheConfig{ Driver: config.Get("CACHE_DRIVER", "memory"), Prefix: config.Get("CACHE_PREFIX", "velocity_cache"), Host: config.Get("REDIS_HOST", "localhost"), Port: config.EnvInt("REDIS_PORT", 6379), Password: config.Get("REDIS_PASSWORD", ""), Database: config.EnvInt("REDIS_DATABASE", 0), } } ``` ### Mail Configuration ```go type MailConfig struct { Driver string Host string Port int Username string Password string Encryption string FromAddress string FromName string } func GetMailConfig() *MailConfig { return &MailConfig{ Driver: config.Get("MAIL_DRIVER", "smtp"), Host: config.Get("MAIL_HOST", "localhost"), Port: config.EnvInt("MAIL_PORT", 1025), Username: config.Get("MAIL_USERNAME", ""), Password: config.Get("MAIL_PASSWORD", ""), Encryption: config.Get("MAIL_ENCRYPTION", "tls"), FromAddress: config.Get("MAIL_FROM_ADDRESS", "noreply@example.com"), FromName: config.Get("MAIL_FROM_NAME", config.Get("APP_NAME", "Velocity")), } } ``` ## Security Best Practices ### Sensitive Data Never commit sensitive data to version control: ```bash # .gitignore .env .env.local .env.production .env.*.local ``` ### Environment Template Provide a template for required variables: ```env # .env.example APP_NAME=Velocity APP_ENV=development APP_DEBUG=true APP_URL=http://localhost:4000 # Database (required) DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE= DB_USERNAME= DB_PASSWORD= # Crypto (required for production) CRYPTO_KEY= # Mail (required for email features) MAIL_DRIVER=smtp MAIL_HOST= MAIL_PORT= MAIL_USERNAME= MAIL_PASSWORD= ``` ### Validation Validate required configuration on startup: ```go func validateConfig() error { required := []string{ "APP_NAME", "DB_HOST", "DB_DATABASE", } for _, key := range required { if os.Getenv(key) == "" { return fmt.Errorf("required environment variable %s is not set", key) } } // Validate crypto key in production if config.Get("APP_ENV", "") == "production" { if os.Getenv("CRYPTO_KEY") == "" { return fmt.Errorf("CRYPTO_KEY is required in production") } } return nil } ``` ## Testing Configuration ### Test Environment Create a `.env.testing` file: ```env APP_ENV=testing APP_DEBUG=true DB_CONNECTION=sqlite DB_DATABASE=:memory: CACHE_DRIVER=memory QUEUE_DRIVER=memory LOG_DRIVER=null ``` ### Load Test Config ```go func TestMain(m *testing.M) { // Load test environment godotenv.Load(".env.testing") // Run tests code := m.Run() os.Exit(code) } ``` ## Docker Integration ### Using Environment Variables ```dockerfile # Dockerfile FROM golang:1.26-alpine WORKDIR /app COPY . . RUN go build -o main . # Environment variables can be passed at runtime CMD ["./main"] ``` ### Docker Compose ```yaml # docker-compose.yml version: '3.8' services: app: build: . ports: - "4000:4000" environment: - APP_NAME=MyApp - APP_ENV=production - APP_DEBUG=false - DB_HOST=db - DB_DATABASE=myapp - DB_USERNAME=root - DB_PASSWORD=secret - REDIS_HOST=redis depends_on: - db - redis db: image: mysql:8 environment: - MYSQL_ROOT_PASSWORD=secret - MYSQL_DATABASE=myapp redis: image: redis:alpine ``` ## Best Practices 1. **Use Environment Variables**: Keep all configuration in environment variables 2. **Provide Defaults**: Always provide sensible defaults for non-sensitive values 3. **Document Variables**: Maintain an `.env.example` file with all available options 4. **Validate on Startup**: Check required configuration before starting the application 5. **Environment-Specific**: Use different `.env` files for different environments 6. **Security**: Never commit `.env` files to version control 7. **Type Safety**: Use `EnvInt` and `EnvBool` for non-string values 8. **Centralize**: Create configuration structs to centralize access patterns ## Examples ### Complete Application Setup ```go package main import ( "fmt" "log" "os" "github.com/joho/godotenv" "github.com/velocitykode/velocity/config" ) type Config struct { App AppConfig Database DatabaseConfig Cache CacheConfig Logging config.LoggingConfig } type AppConfig struct { Name string Environment string Debug bool URL string Port int } type DatabaseConfig struct { Host string Port int Database string Username string Password string } type CacheConfig struct { Driver string Prefix string } func LoadAppConfig() (*Config, error) { // Load .env file if err := godotenv.Load(); err != nil { log.Println("No .env file found") } // Validate required variables if err := validateConfig(); err != nil { return nil, err } return &Config{ App: AppConfig{ Name: config.Get("APP_NAME", "Velocity"), Environment: config.Get("APP_ENV", "development"), Debug: config.EnvBool("APP_DEBUG", false), URL: config.Get("APP_URL", "http://localhost:4000"), Port: config.EnvInt("APP_PORT", 4000), }, Database: DatabaseConfig{ Host: config.Get("DB_HOST", "localhost"), Port: config.EnvInt("DB_PORT", 3306), Database: config.Get("DB_DATABASE", "velocity"), Username: config.Get("DB_USERNAME", "root"), Password: config.Get("DB_PASSWORD", ""), }, Cache: CacheConfig{ Driver: config.Get("CACHE_DRIVER", "memory"), Prefix: config.Get("CACHE_PREFIX", "velocity_cache"), }, Logging: config.GetLoggingConfig(), }, nil } func validateConfig() error { required := []string{"DB_DATABASE"} for _, key := range required { if os.Getenv(key) == "" { return fmt.Errorf("%s is required", key) } } return nil } func main() { cfg, err := LoadAppConfig() if err != nil { log.Fatal("Failed to load configuration:", err) } fmt.Printf("Starting %s in %s mode on port %d\n", cfg.App.Name, cfg.App.Environment, cfg.App.Port, ) } ``` ================================================================================ # Authentication Source: https://vel.build/docs/core/authentication/ Section: Core Framework Summary: Implement user login, registration, password hashing, and session management with Velocity's auth system. Velocity provides a powerful authentication system that handles user login, registration, password hashing, and session management out of the box. ## Setup ### Environment Configuration Configure authentication in your `.env` file: ```env # Crypto settings (required for session encryption) CRYPTO_KEY=base64:your-32-byte-base64-encoded-key CRYPTO_CIPHER=AES-256-CBC # Auth settings AUTH_GUARD=web AUTH_MODEL=User HASH_BCRYPT_COST=10 # Session settings SESSION_NAME=velocity_session SESSION_LIFETIME=120 SESSION_PATH=/ SESSION_SECURE=true SESSION_HTTP_ONLY=true SESSION_SAME_SITE=lax ``` ### Initialization When you boot the app via `velocity.New()`, the framework reads `AUTH_GUARD`, `HASH_BCRYPT_COST`, and the `SESSION_*` variables, builds an `auth.Manager`, registers an ORM-backed user provider, and wires a `SessionGuard` against the encrypted-cookie store. No manual wiring is required for the common case. If you need to construct the manager yourself (custom guard, embedded use, tests), the underlying API is: ```go package main import ( "database/sql" "net/http" "github.com/velocitykode/velocity/auth" "github.com/velocitykode/velocity/auth/drivers/guards" "github.com/velocitykode/velocity/crypto" ) func buildAuth(db *sql.DB, enc crypto.Encryptor) (*auth.Manager, error) { manager := auth.NewManager() // Provider: ORM-backed user lookup against the "users" table. provider := auth.NewORMUserProvider(db, "User", manager.GetHasher()) manager.RegisterProvider("users", provider) // Guard: encrypted-cookie session store. sessionGuard, err := guards.NewSessionGuard(provider, auth.SessionConfig{ Name: "velocity_session", Lifetime: 120, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, }, enc) if err != nil { return nil, err } manager.RegisterGuard("web", sessionGuard) manager.SetDefaultGuard("web") return manager, nil } ``` Inside a handler you reach the manager through `auth.FromContext(ctx)`: ```go import "github.com/velocitykode/velocity/auth" m := auth.FromContext(ctx) // *auth.Manager, or nil if auth is not configured ``` ### User Model Requirements Your User model must implement the `Authenticatable` interface: ```go type User struct { orm.Model[User] Name string `orm:"column:name" json:"name"` Email string `orm:"column:email" json:"email"` Password string `orm:"column:password" json:"-"` } // GetAuthIdentifier returns the user's unique identifier func (u *User) GetAuthIdentifier() interface{} { return u.ID } // GetAuthPassword returns the user's hashed password func (u *User) GetAuthPassword() string { return u.Password } // GetRememberToken returns the remember token func (u *User) GetRememberToken() string { return "" // Implement if using remember me } // SetRememberToken sets the remember token func (u *User) SetRememberToken(token string) { // Implement if using remember me } ``` ## Quick Start Using authentication in handlers: ```go import ( "github.com/velocitykode/velocity/auth" "github.com/velocitykode/velocity/router" "github.com/velocitykode/velocity/view" ) func (c *AuthHandler) Login(ctx *router.Context) error { var formData struct { Email string `json:"email"` Password string `json:"password"` Remember bool `json:"remember"` } if err := ctx.Bind(&formData); err != nil { formData.Email = ctx.Request.FormValue("email") formData.Password = ctx.Request.FormValue("password") formData.Remember = ctx.Request.FormValue("remember") == "on" } credentials := map[string]interface{}{ "email": formData.Email, "password": formData.Password, } m := auth.FromContext(ctx) success, _ := m.Attempt(ctx.Response, ctx.Request, credentials, formData.Remember) if success { view.Location(ctx.Response, ctx.Request, "/dashboard") } else { view.Render(ctx.Response, ctx.Request, "Auth/Login", view.Props{ "errors": map[string]string{ "email": "These credentials do not match our records.", }, }) } return nil } ``` ## User Authentication ### Login Attempts ```go m := auth.FromContext(ctx) credentials := map[string]interface{}{ "email": "user@example.com", "password": "secret123", } success, err := m.Attempt(ctx.Response, ctx.Request, credentials, false) if err != nil { // err is auth.ErrLoginThrottled when the configured throttler rejected // the attempt before credentials were even checked. return err } if success { user := m.User(ctx.Request) log.Info("User logged in", "user_id", user.GetAuthIdentifier()) } ``` ### Remember Me Functionality ```go // Login with "remember me" for extended sessions m := auth.FromContext(ctx) success, _ := m.Attempt(ctx.Response, ctx.Request, credentials, true) if success { user := m.User(ctx.Request) log.Info("User logged in with remember me", "user_id", user.GetAuthIdentifier()) } ``` ### Checking Authentication Status ```go m := auth.FromContext(ctx) if m.Check(ctx.Request) { user := m.User(ctx.Request) if user != nil { log.Info("Authenticated user", "user_id", user.GetAuthIdentifier()) } } else { return ctx.Redirect(http.StatusFound, "/login") } ``` ### Logout ```go func LogoutHandler(ctx *router.Context) error { m := auth.FromContext(ctx) if err := m.Logout(ctx.Response, ctx.Request); err != nil { return err } view.Location(ctx.Response, ctx.Request, "/login") return nil } ``` ## Password Hashing Password hashing lives on the manager so the bcrypt cost configured in `HASH_BCRYPT_COST` is honored uniformly. ### Hash Passwords ```go m := auth.FromContext(ctx) password := "user_password_123" hashedPassword, err := m.Hash(password) if err != nil { log.Error("Failed to hash password", "error", err) return err } // Store hashedPassword in database user.Password = hashedPassword ``` ### Verify Passwords ```go m := auth.FromContext(ctx) if m.Verify(providedPassword, user.Password) { log.Info("Password verification successful") } else { log.Warn("Password verification failed") } ``` If you need a hasher outside a request (a CLI seeder, for example), construct one directly with `auth.NewBcryptHasher(cost)` and call `Hash` / `Verify` on it. The minimum cost is clamped to 10 with a warning. ## User Interface ### Authenticatable Interface Implement the `Authenticatable` interface for your user models: ```go type User struct { ID uint `json:"id"` Email string `json:"email"` Password string `json:"-"` // Hidden from JSON Name string `json:"name"` } // GetAuthIdentifier returns the user's unique identifier func (u *User) GetAuthIdentifier() interface{} { return u.ID } // GetAuthPassword returns the user's hashed password func (u *User) GetAuthPassword() string { return u.Password } // GetRememberToken returns the remember token func (u *User) GetRememberToken() string { return "" } // SetRememberToken sets the remember token func (u *User) SetRememberToken(token string) {} ``` ### Custom User Providers ```go // Implement UserProvider interface for custom user retrieval type CustomUserProvider struct { db *sql.DB } func (p *CustomUserProvider) FindByID(id interface{}) (auth.Authenticatable, error) { var user User err := p.db.QueryRow("SELECT id, email, password, name FROM users WHERE id = ?", id). Scan(&user.ID, &user.Email, &user.Password, &user.Name) if err != nil { return nil, err } return &user, nil } func (p *CustomUserProvider) FindByCredentials(credentials map[string]interface{}) (auth.Authenticatable, error) { email := credentials["email"].(string) var user User err := p.db.QueryRow("SELECT id, email, password, name FROM users WHERE email = ?", email). Scan(&user.ID, &user.Email, &user.Password, &user.Name) if err != nil { return nil, err } return &user, nil } func (p *CustomUserProvider) ValidateCredentials(user auth.Authenticatable, credentials map[string]interface{}) bool { password, _ := credentials["password"].(string) return auth.NewBcryptHasher(10).Verify(password, user.GetAuthPassword()) } func (p *CustomUserProvider) UpdateRememberToken(user auth.Authenticatable, token string) error { user.SetRememberToken(token) _, err := p.db.Exec("UPDATE users SET remember_token = ? WHERE id = ?", token, user.GetAuthIdentifier()) return err } ``` ## Middleware Integration ### Auth Middleware Use `auth.AuthMiddleware` to require authentication on a route. It returns 401 JSON for API requests and redirects to `/login?redirect=...` for HTML requests. ```go import "github.com/velocitykode/velocity/auth" r.Get("/dashboard", dashboardHandler.Index, auth.AuthMiddleware(manager)) ``` For role- or ability-based gates, the package also exposes `auth.RequireRole`, `auth.RequireAnyRole`, `auth.RequireAllRoles`, and `auth.AuthorizeMiddleware`. All of them deny with 401 when the request is unauthenticated and 403 when the policy fails. ### Guest Middleware `auth.GuestMiddleware` blocks already-authenticated users from login/register pages. Pass a redirect path with `auth.GuestMiddlewareWithRedirect`. ```go r.Get("/login", authHandler.ShowLoginForm, auth.GuestMiddlewareWithRedirect(manager, "/dashboard")) r.Get("/register", authHandler.ShowRegisterForm, auth.GuestMiddlewareWithRedirect(manager, "/dashboard")) ``` ## Session Management ### Session Configuration Configure sessions in your `.env` file: ```env # Session settings SESSION_NAME=velocity_session SESSION_LIFETIME=120 # Minutes SESSION_PATH=/ SESSION_DOMAIN= SESSION_SECURE=true # HTTPS only SESSION_HTTP_ONLY=true # No JavaScript access SESSION_SAME_SITE=lax # CSRF protection ``` ### Session Backends Cookie-encrypted sessions are the default, but they are not the only option. The framework defines a `SessionStore` interface so you can swap in a server-side store (for example a Redis- or DB-backed implementation) without changing handler code. The interface lives in `auth/session.go`: ```go // auth.Session is the value handed to handlers. type Session interface { ID() string Get(key string) interface{} Put(key string, value interface{}) Has(key string) bool Remove(key string) Clear() Regenerate() error Invalidate() error Flash(key string, value interface{}) GetFlash(key string) interface{} Save(w http.ResponseWriter) error } // auth.SessionStore is what backends implement. type SessionStore interface { Create(id string) (Session, error) Get(r *http.Request, id string) (Session, error) Save(w http.ResponseWriter, session Session) error Destroy(id string) error GarbageCollect(maxLifetime time.Duration) error } ``` The shipped implementation is `auth/drivers/session.CookieStore` (encrypted cookies, with `auth.SessionConfig` controlling cookie attributes). To plug in a custom backend, implement `SessionStore`, construct a `SessionGuard` against it, and register that guard with the manager. `SessionGuard` accepts whichever store it is given because it talks to the interface, not the cookie struct directly. For ad-hoc reads you can also call `auth.GetSessionFromRequest(r, store, cookieName)` to resolve a session from a request when you have a store reference outside of guard code. `auth.SessionConfig.Validate(env)` enforces safe defaults: `HttpOnly` must be true unless `AllowJSAccess` is explicitly set, `Secure` must be true outside `testing`/`development`, `SameSite` must be non-zero, and `SameSite=None` requires `Secure=true`. Failing this returns `auth.ErrInsecureSessionConfig`, so bootstrap code can fail fast in production and log-then-continue in dev. #### Server-side session store The cookie-side `SessionStore` carries per-request state, but it cannot answer two product questions you will eventually need to answer: "log me out everywhere" and "show me my active devices." Both require the server to know which session ids belong to which user, which a stateless cookie cannot tell you. The framework exposes a parallel `auth.ServerSessionStore` interface for that record: ```go // auth.ServerSessionStore (auth/server_session_store.go) type ServerSessionStore interface { Get(ctx context.Context, id string) (*StoredSession, error) Put(ctx context.Context, session *StoredSession) error Delete(ctx context.Context, id string) error DeleteAllForUser(ctx context.Context, userID string) error ListForUser(ctx context.Context, userID string) ([]*SessionMeta, error) } ``` `auth.StoredSession` is the full record (`ID`, `UserID`, `Data map[string]any`, `CreatedAt`, `LastSeenAt`, `ExpiresAt`, `IPAddress`, `UserAgent`). `auth.SessionMeta` is the listing-only projection: same fields minus `Data`, so administrative listings cannot leak per-session payloads. Sentinel errors are `auth.ErrSessionNotFound`, `auth.ErrSessionExpired` (returned by `Get` after evicting the expired record), and `auth.ErrNoServerSessionStore` (returned by the manager helpers below when no store is installed). You usually want both. The encrypted cookie store handles per-request reads and writes with no I/O. The server store underwrites administrative operations only, without it `RevokeSession` and `ListActiveSessions` return `ErrNoServerSessionStore`. The shipped driver is `auth/drivers/session.NewMemoryStore`, an in-process implementation suitable for development, tests, and single-process deployments. It is `sync.RWMutex`-protected, maintains a secondary `userID -> {sessionID}` index so `DeleteAllForUser` and `ListForUser` are O(sessions-for-user), and runs a background sweep goroutine (default cadence 1 minute, override with `session.WithSweepInterval(d)`) that reaps expired records. The sweep is started by `NewMemoryStore` via `async.Go`, so a panic inside the loop is reported through the framework panic handler rather than crashing the process. `Close(ctx)` stops the sweep and is idempotent (safe to call multiple times). Production multi-process deployments should provide a Redis- or DB-backed driver against the same interface. Wire it once at bootstrap and the manager helpers light up: ```go import ( "context" "github.com/velocitykode/velocity/auth" "github.com/velocitykode/velocity/auth/drivers/session" ) store := session.NewMemoryStore() // or session.NewMemoryStore(session.WithSweepInterval(30*time.Second)) manager.SetServerSessionStore(store) ``` Once installed, three methods on `*auth.Manager` cover the administrative surface: - `RevokeSession(ctx, sessionID) error`: single-session logout (e.g. "log out this device"). - `RevokeAllSessions(ctx, userID) error`: bulk revoke (e.g. "log out everywhere", post-password-change). - `ListActiveSessions(ctx, userID) ([]*SessionMeta, error)`: feed the "your devices" UI. All three return `auth.ErrNoServerSessionStore` when no store is configured, so callers can branch on missing capability without a nil-check dance. `SetServerSessionStore(nil)` removes a previously installed store. ##### Recipe: Log out all sessions on password change **When:** A user changes their password from the account settings page. Anyone holding a session cookie issued before the change should be evicted, including other browsers, mobile apps, and the attacker the user is currently kicking out. **Code:** ```go func (h *AccountHandler) ChangePassword(ctx *router.Context) error { m := auth.FromContext(ctx) user := m.User(ctx.Request) if user == nil { return ctx.Error("unauthorized", http.StatusUnauthorized) } // ... validate current password, hash new one, persist ... userID := fmt.Sprint(user.GetAuthIdentifier()) if err := m.RevokeAllSessions(ctx.Request.Context(), userID); err != nil && !errors.Is(err, auth.ErrNoServerSessionStore) { return err } // The current request's cookie is also gone now; re-issue a session // for this device so the user is not bounced to /login mid-flow. credentials := map[string]interface{}{ "email": user.(*models.User).Email, "password": newPassword, } _, _ = m.Attempt(ctx.Response, ctx.Request, credentials, false) return nil } ``` **Why this shape:** `RevokeAllSessions` walks the secondary `userID -> {sessionID}` index and deletes every record in one shot, so the call is cheap even for users with many devices. Tolerating `ErrNoServerSessionStore` keeps the same handler usable in environments that have not yet provisioned a server-side store (e.g. local dev). Re-attempting after the bulk revoke gives the current request a fresh cookie tied to a brand-new server-side record, which is what you want: the password change should not log out the device performing it. ### LoginThrottler `SessionGuard.Attempt` (and `JWTGuard.Attempt`) consult a `contract.LoginThrottler` before checking credentials. The interface is the seam for credential-stuffing defense: ```go // contract.LoginThrottler type LoginThrottler interface { Allow(r *http.Request, key string) bool RecordFailure(r *http.Request, key string) RecordSuccess(r *http.Request, key string) } ``` Contract: - `Allow(r, key)` runs before the credential check. Returning `false` short-circuits the attempt with `auth.ErrLoginThrottled`. - `RecordFailure(r, key)` runs when credential validation fails. - `RecordSuccess(r, key)` runs after a successful login; a good implementation clears the failure counter for that key. The default throttler is `auth.NoopLoginThrottler{}`, which permits every attempt. Install a real one with `guard.SetLoginThrottler(yourThrottler)` (passing `nil` reverts to the no-op). The framework also exposes `auth.ThrottleKey(r, credentials)`, which derives the rate-limit key as `"|"`. The `identifier` is the first non-empty value among `email`, `username`, `name`, `login` in the credentials map, falling back to IP-only when no identifier is present. Use it so a custom guard wrapper produces keys consistent with the built-in guards. ## Two-factor authentication Velocity ships RFC 6238 TOTP (time-based one-time passwords) plus single-use recovery codes. The surface lives in `auth/totp.go`; everything is HMAC-SHA1, 6-digit, 30-second period by default, matching what Google Authenticator, 1Password, Authy, and friends speak out of the box. The package-level `auth.TOTP` is a pre-configured `*TOTPGenerator` with `Skew: 1` (previous, current, and next windows are accepted on verify), which is the right default for almost every app. Construct your own with `auth.NewTOTP(auth.TOTPConfig{...})` if you need to override `Issuer`, `Digits`, `Period`, or `Skew`. ### Enrollment Enrollment is two server round-trips: generate a secret, render its `otpauth://` URI as a QR code, then verify the first code the user types from their authenticator app before persisting the secret as enabled. ```go import "github.com/velocitykode/velocity/auth" // 1. Begin enrollment: generate a secret and the otpauth:// URI. secret, qrURL, err := auth.TOTP.Generate("user@example.com") if err != nil { return err } // `secret` is base32 (no padding); show qrURL to the user as a QR code, // and stash secret in a pending-enrollment record (NOT on the user yet). // 2. User scans the QR with their authenticator and submits the first // 6-digit code. Use VerifyAndConsume so the matched step gets recorded // and replay is rejected from the very first verify. matched, step := auth.TOTP.VerifyAndConsume(secret, submittedCode, 0) if !matched { return errors.New("invalid code") } // Persist secret + step on the user, flip `two_factor_enabled = true`. user.TOTPSecret = secret user.TOTPLastUsedStep = step ``` `Generate(label)` returns `(secret, qrURL, err)` where `secret` is a base32-encoded 160-bit value (RFC 6238 section 5.1) and `qrURL` is an `otpauth://totp/