# 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/ # app.Bootstrap: middleware, modules, event listeners │ ├── 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" "myapp/internal/app" "myapp/routes" "github.com/velocitykode/velocity" ) func main() { v, err := velocity.New() if err != nil { log.Fatal(err) } if err := app.Bootstrap(v); err != nil { log.Fatal(err) } routes.Register(v) if err := v.Serve(); err != nil { log.Fatal(err) } } ``` `velocity.New()` builds the application container (logger, crypto, DB, cache, queue, router, …) and returns an `*velocity.App`. `app.Bootstrap(v)` is your own bootstrap function (scaffolded into `internal/app`) where you configure middleware, modules, and event listeners. `routes.Register(v)` registers your routes against `v.Router`, and `v.Serve()` starts the HTTP server. `*velocity.App` also exposes a fluent bootstrap chain - `v.Modules(...)`, `v.Middleware(...)`, `v.Routes(...)`, `v.Events(...)`, `v.Schedule(...)`, `v.Commands(...)`, and `v.Exceptions(...)` - if you prefer to wire everything from `main.go`. Call `v.Run()` to dispatch a `vel ...` command from `os.Args`, or `v.Serve()` to start the server. Define routes in `routes/web.go`: ```go package routes import ( "myapp/internal/handlers" "github.com/velocitykode/velocity" ) func Register(v *velocity.App) { r := v.Router r.Get("/", handlers.Home) } ``` Handlers have the signature `func(ctx *router.Context) error`: ```go package handlers import ( "github.com/velocitykode/velocity/router" ) func Home(ctx *router.Context) error { return ctx.String(200, "Welcome to Velocity!") } ``` 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=debug # Encryption / signing - installer populates these at scaffold time APP_KEY= QUEUE_SIGNING_KEY= AUTH_JWT_SECRET= CRYPTO_CIPHER=AES-256-GCM # 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 # memory, file, redis, database ``` `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 key, base64-encodes it with a `base64:` prefix, and writes it to `APP_KEY` in `.env` (creating the file if it doesn't exist) - 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 ================================================================================ # Standalone Packages Source: https://vel.build/docs/getting-started/standalone/ Section: Getting Started Summary: Cherry-pick individual Velocity components. Import a single subsystem, construct its Manager directly, and own the lifecycle without velocity.New(). Velocity is a single Go module, but its subsystems are designed as independent packages. You do not have to call `velocity.New()` to use them. Import the one package you need (cache, crypto, validation, httpclient, str, log, pipeline, collect, async), construct it directly, and own its lifecycle yourself. This is ideal for libraries, CLIs, one-off scripts, tests, and MCP servers that want Velocity's helpers without booting a full HTTP application. ## Install Velocity is one module. Add it to your project with `go get`: ```bash go get github.com/velocitykode/velocity@latest ``` The module path is `github.com/velocitykode/velocity`. Every subsystem is a subpackage under it (for example `github.com/velocitykode/velocity/cache`), so a single `go get` makes all of them importable. The Go toolchain only compiles the packages you actually import, so pulling in `str` does not drag in the ORM or the router. **Standalone vs full app.** When you import a single package and call its constructor yourself, *you own the lifecycle*: you build the config, hold the instance, and decide when to use it. When you call `velocity.New()` instead, the framework reads your `.env` and `config/` files and wires every subsystem (logger, crypto, cache, queue, router, ...) into an `*velocity.App` for you. Both paths use the same underlying constructors shown on this page; the full app is just the batteries-included assembly of them. See for the full app path. ## Standard driver bundles Several subsystems are *pluggable*: cache, log, orm, queue, and storage each own a driver registry, and config selects a driver by name at runtime. A driver only becomes selectable once its factory has been registered, and registration happens in a package's `init()` at import time. The light built-in drivers self-register from the subsystem's own package, so importing the subsystem makes them available with no extra work. The heavier drivers live in separate leaf packages so they (and their dependencies) are only compiled when you ask for them. To register *every* driver in a subsystem with one line, blank-import its `standard` bundle: ```go import ( _ "github.com/velocitykode/velocity/cache/standard" _ "github.com/velocitykode/velocity/log/standard" _ "github.com/velocitykode/velocity/orm/standard" _ "github.com/velocitykode/velocity/queue/standard" _ "github.com/velocitykode/velocity/storage/standard" ) ``` Each bundle wires its subsystem's full driver set into the registry so config can pick any of them at runtime: | Bundle | Light drivers (self-register from the subsystem) | Heavy leaf drivers the bundle adds | |---|---|---| | `cache/standard` | memory, file | redis (`cache/redis`, go-redis) | | `log/standard` | console, null | file, daily (`log/file`), stack (`log/stack`) | | `orm/standard` | modernc SQLite (pure-Go default) | mysql (`orm/mysql`), postgres (`orm/postgres`), cgo SQLite (`orm/sqlite`) | | `queue/standard` | memory, database | redis (`queue/redis`, go-redis) | | `storage/standard` | local, memory | s3 (`storage/s3`, AWS SDK) | **Smaller footprint?** If you only need one heavy driver, skip the bundle and blank-import just that leaf (for example `_ "github.com/velocitykode/velocity/cache/redis"`). You only pay for the dependencies you import. The bundles exist purely to register the full set in one line; they must not be imported by the framework core, which is why they live in their own packages. ## Cherry-picking components Every Velocity subsystem exposes a plain Go constructor. Below are the real signatures for the most commonly used leaf packages, each shown in isolation with no `velocity.App`. ### Cache `cache.NewManager(config *cache.Config) *cache.Manager`. The memory, file, and database drivers self-register from the `cache` package's own `init()`, so a memory store needs no bundle import at all: ```go package main import ( "fmt" "time" "github.com/velocitykode/velocity/cache" ) func main() { mgr := cache.NewManager(&cache.Config{ Default: "memory", Stores: map[string]cache.StoreConfig{ "memory": {Driver: cache.DriverMemory}, }, }) mgr.Put("greeting", "hello", 5*time.Minute) if v, found := mgr.Get("greeting"); found { fmt.Println(v) // hello } } ``` To use Redis here you would add `_ "github.com/velocitykode/velocity/cache/standard"` (or the `cache/redis` leaf) and set `Driver: cache.DriverRedis` with the host fields on `StoreConfig`. **Thread the context.** `Manager` exposes a `*WithContext` variant for every method (`PutWithContext`, `GetWithContext`, `RememberEWithContext`, `StoreWithContext`, ...). Stores are created lazily on first use; passing a `context.Context` lets a slow driver connect (a Redis dial) honour your deadline. Prefer the context variants in any code that already has one. See . ### Crypto `crypto.NewEncryptor(config crypto.Config) (crypto.Encryptor, error)`. The crypto package never reads the environment; you pass the key explicitly. Build one encryptor and reuse it: ```go package main import ( "fmt" "github.com/velocitykode/velocity/crypto" ) func main() { enc, err := crypto.NewEncryptor(crypto.Config{ Key: "base64:your-base64-encoded-key-here", Cipher: "AES-256-GCM", }) if err != nil { panic(err) } payload, err := enc.Encrypt("sensitive data") if err != nil { panic(err) } plaintext, err := enc.Decrypt(payload) if err != nil { panic(err) } fmt.Println(plaintext) // sensitive data } ``` `crypto.Config` fields are `Key`, `Cipher`, and `PreviousKeys` (for key rotation). See . ### Validation `validation.NewValidator() validation.Validator`. Rules are a `map[string][]string` (`validation.Rules`); each entry is the field name mapped to its list of rule strings. `Validate` returns a `*validation.ValidatedData`: ```go package main import ( "fmt" "github.com/velocitykode/velocity/validation" ) func main() { v := validation.NewValidator() data := map[string]interface{}{ "name": "Ada", "email": "ada@example.com", } rules := validation.Rules{ "name": {"required", "min:2"}, "email": {"required", "email"}, } validated, err := v.Validate(data, rules) if err != nil { panic(err) } if validated.Errors().HasError("email") { fmt.Println("email is invalid") } } ``` ### HTTP client `httpclient.New(opts ...httpclient.Option) *httpclient.Client`. The client ships with secure defaults: TLS 1.2 or higher, a capped redirect chain, sensitive headers stripped on cross-host redirects, a 30 second timeout, and the SSRF private-IP dial guard enabled. Every request takes a `context.Context`: ```go package main import ( "context" "io" "time" "github.com/velocitykode/velocity/httpclient" ) func main() { client := httpclient.New( httpclient.WithBaseURL("https://api.example.com"), httpclient.WithTimeout(10*time.Second), ) ctx := context.Background() resp, err := client.Get(ctx, "/health") if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) _ = body } ``` Use `httpclient.WithAllowedHosts(...)` to whitelist specific internal hosts. `Client` also exposes `Post`, `Put`, `Delete`, and `Do(ctx, *http.Request)` for full control. ### Strings The `str` package is pure functions plus a fluent `*str.Stringable` wrapper. There is no manager to construct; import and call: ```go package main import ( "fmt" "github.com/velocitykode/velocity/str" ) func main() { fmt.Println(str.Slug("Hello World")) // hello-world fmt.Println(str.Camel("user_name")) // userName fmt.Println(str.Snake("UserName")) // user_name // Fluent chain via str.Of(...). out := str.Of(" Hello World "). Camel(). ToString() fmt.Println(out) } ``` ### Collections `collect` mirrors `str`: standalone generic helpers over slices plus a fluent `*collect.Collection[T]` from `collect.From(items)`: ```go package main import ( "fmt" "github.com/velocitykode/velocity/collect" ) func main() { nums := []int{1, 2, 3, 4, 5} evens := collect.Filter(nums, func(n int) bool { return n%2 == 0 }) doubled := collect.Map(nums, func(n int) int { return n * 2 }) fmt.Println(evens) // [2 4] fmt.Println(doubled) // [2 4 6 8 10] } ``` ### Pipeline `pipeline.New[T any]() *pipeline.Pipeline[T]` sends a value through a series of stages. A stage is anything implementing `Handle(passable T, next func(T) error) error`; wrap a function with `pipeline.Pipe[T]`. `Then` runs the pipeline: ```go package main import ( "fmt" "strings" "github.com/velocitykode/velocity/pipeline" ) func main() { err := pipeline.New[string](). Send(" hello "). Through( pipeline.Pipe[string](func(s string, next func(string) error) error { return next(strings.TrimSpace(s)) }), pipeline.Pipe[string](func(s string, next func(string) error) error { return next(strings.ToUpper(s)) }), ). Then(func(s string) error { fmt.Println(s) // HELLO return nil }) if err != nil { panic(err) } } ``` `Pipeline` is not safe for concurrent use: build and run it from a single goroutine. ### Async The `async` package is package-level generic helpers; there is nothing to construct. `async.Run` runs a function on a goroutine and returns a `*async.Result[T]`; `async.All` / `async.Map` fan out over a set of functions or items: ```go package main import ( "fmt" "github.com/velocitykode/velocity/async" ) func main() { // Run a single task off the calling goroutine. r := async.Run(func() int { return 21 * 2 }) value, err := r.Get() if err != nil { panic(err) } fmt.Println(value) // 42 // Run several functions concurrently and collect results. results, allErr := async.All( func() int { return 1 }, func() int { return 2 }, func() int { return 3 }, ) if allErr != nil { panic(allErr) } fmt.Println(results) // [1 2 3] } ``` Context-aware variants exist where blocking work needs a deadline: `async.RunWithTimeout(timeout, fn)`, `async.RunWithContext(ctx, fn)`, and `async.GoCtx(ctx, fn)`. Use them when the work can hang and you want cancellation to propagate. ### Log `log.NewManager(cfg log.LoggingConfig) *log.Manager`. The console and null drivers self-register from the `log` package, so a console logger needs no bundle import. `Channel(name)` (or `Default()`) returns a `Logger` with `Debug`, `Info`, `Warn`, and `Error(msg string, kvs ...any)`: ```go package main import ( "github.com/velocitykode/velocity/log" ) func main() { mgr := log.NewManager(log.LoggingConfig{ Default: "console", Channels: map[string]log.ChannelConfig{ "console": {Driver: "console", Level: "debug"}, }, }) logger, err := mgr.Default() if err != nil { panic(err) } logger.Info("standalone logger ready", "driver", "console") } ``` For the `file`, `daily`, or `stack` drivers, blank-import `_ "github.com/velocitykode/velocity/log/standard"` (or the individual leaf) so those factories are registered before you resolve the channel. ## When to reach for the full app Standalone construction is the right tool for libraries, CLIs, scripts, tests, and embedded use. Reach for `velocity.New()` when you want the framework to read `.env` and `config/`, build all subsystems from that config, and hand you a wired `*velocity.App` (with `app.Cache`, `app.Log`, the router, and the rest). The constructors are identical either way; the full app simply assembles them for you. See to scaffold a complete application. ================================================================================ # 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 (the installer checks `go version` on startup and refuses to run on an older toolchain) - **Node.js 20+** - Required for frontend asset compilation (Vite 7) - **Git** - Required for project initialization (each new project is initialized as a Git repository, and Git clone is used as a fallback when the template tarball download is unavailable) Full-stack projects install JavaScript dependencies with [bun](https://bun.sh) when it is available, falling back to `npm`. Installing bun is optional but gives much faster installs. ## Install via Homebrew The recommended way to install Velocity on macOS is through the Homebrew cask: ```bash brew tap velocitykode/tap brew install velocity ``` ## Verify Installation Check that the CLI is installed correctly: ```bash velocity --version ``` You should see the installer version followed by the template tags pinned to that release: ``` velocity 0.21.14 templates: api -> v... react -> v... vue -> v... ``` Each installer release pins exact template tags, so the version output lists those tags as the relevant build coordinates. ## 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 from the pinned template 2. Installs dependencies (`go mod tidy`, plus bun/npm for full-stack projects) 3. Builds the `./vel` binary from your project source 4. Runs initial database migrations When the project is ready it prints the next steps to start the dev servers (`cd myapp` then `./vel serve`). If the database server is not reachable, migrations are skipped and the printed steps include starting your database and running `./vel migrate` manually. ## 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 ``` ## Setting Project Defaults Use `velocity config` to set defaults that `velocity new` will apply when a flag is not provided: ```bash velocity config set default.database postgres velocity config set default.cache redis velocity config set default.api true ``` Supported keys are `default.database`, `default.cache`, `default.queue`, `default.auth`, and `default.api`. Read or inspect the current values with: ```bash velocity config get default.database velocity config list velocity config reset ``` Configuration is stored in `~/.vel/config.yaml`. ## Updating ### Update Velocity Installer For Homebrew installs, upgrade the cask: ```bash brew upgrade --cask velocity ``` The built-in self-update command detects a Homebrew install and will point you at the `brew upgrade --cask velocity` command above. It only downloads and replaces the binary in place for non-Homebrew (manual) installs: ```bash velocity self-update ``` ### Rebuild vel The `vel` binary is built from the project's `main.go`. To rebuild it manually from the project root: ```bash go build -o vel . ``` ## Uninstalling ### Remove Velocity Installer ```bash brew uninstall --cask 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, routes, 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/). ## Command grammar Command names are plain words separated by spaces: `migrate`, `migrate fresh`, `gen model`, `gen grpc service`. Nothing in the CLI uses a colon. The dispatcher joins the leading arguments into a candidate name (at most three words, the length of the longest registered name) and resolves longest match first: - A subcommand beats its bare parent. `vel migrate fresh` runs the fresh command; it is never `migrate` with a `fresh` argument. - Token joining stops at the first flag-like argument, so `vel migrate --pretend` resolves to `migrate` with `--pretend` handed through as an argument, and `vel run seed` resolves to `run` with `seed` (plus any trailing arguments) passed to your custom command. - An unknown command reports the full unmatched token sequence: `vel migrate frsh` fails with `vel: unknown command "migrate frsh"`, not just `migrate`. Unrecognised arguments are rejected rather than silently dropped. An unknown flag errors with `unknown flag: `, a stray positional with `unexpected argument: `, and a value-taking flag with nothing after it with `flag needs a value`. Value-taking flags accept both `--flag value` and `--flag=value`. Arguments are parsed **before** the application bootstraps, so a typo fails immediately without running your module lifecycle. ## Server ### vel serve Start the development server with live reload. ```bash vel serve [flags] ``` | Flag | Short | Default | Description | | ----------- | ----- | ------------- | ----------------------------------------- | | `--port` | `-p` | `4000` | HTTP port (falls back to `APP_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. `.env` is loaded and `APP_PORT` / `APP_ENV` are read; flags override both. With no environment resolved, `vel serve` defaults to `development` and warns that `APP_ENV` was unset. 2. When a `package.json` is present, the Vite dev server is started with `npm run dev` (or `bun run dev` when `bun` is on `PATH` and a `bun.lock` file exists). 3. The Go app compiles to `.vel/tmp/server`, which is created owner-only because the binary embeds build-time configuration. 4. `.go` files are watched; a change debounces for 500ms, then rebuilds and restarts the server. The rebuild also refreshes the project's `./vel` binary, so one-shot commands in another terminal (`vel routes`, `vel migrate`, `vel gen ...`) see current source. `vel serve run` is the internal entry point the watcher uses to launch the compiled child process. It's dispatchable but not meant to be typed by hand, so it's omitted from `vel help`. ### vel build Compile a production binary. ```bash vel build [flags] ``` | Flag | Short | Default | Description | | ----------- | ----- | ---------------- | ---------------------------------------------------- | | `--output` | `-o` | project dir name | Output path (`.exe` appended when `--os windows`) | | `--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 ``` The build runs with `CGO_ENABLED=0` and stamps version metadata into `velocity.BuildInfo` via `-ldflags` (`Version`, `Commit`, `Date`). `Version` defaults to `devel` and `Commit` to the short SHA from `git rev-parse --short HEAD`, falling back to `devel` when git is unavailable. ## 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. It is the only flag `migrate` accepts; anything else errors. With no database configured (`DB_CONNECTION` unset) the command warns and exits cleanly instead of failing. ### vel migrate fresh Drop all tables, then run every migration from scratch. ```bash vel migrate fresh [--force] ``` In a production-class environment this command refuses to run unless `--force` (`-f`) is passed. The guard treats `production`, `prod`, `staging`, and any unrecognised `APP_ENV` value as production, so a typo'd `APP_ENV` cannot disable it. `development`, `dev`, `test`, `testing`, `local`, and an unset `APP_ENV` are non-production. `--force` / `-f` is the only argument this command accepts. Destructive - deletes all data. Development / testing only. ### vel migrate rollback Roll back the most recent batch of migrations. ```bash vel migrate rollback [--step N] [--force] ``` | Flag | Short | Default | Description | | --------- | ----- | ------- | ------------------------------------------------------------- | | `--step` | `-s` | `1` | Number of batches to roll back (must be >= 1) | | `--force` | `-f` | off | Bypass the production-environment guard | `--step N`, `-s N`, and `--step=N` are equivalent and compose with `--force` in any order. A non-integer or below-1 value errors. Like `migrate fresh`, this is gated in production-class environments; pass `--force` to proceed. ### vel migrate status Show which migrations have run. ```bash vel migrate status ``` Takes no arguments. ### vel db wipe Drop every table in the current database without running migrations. ```bash vel db wipe [--force] ``` In a production-class environment this command refuses to run unless `--force` (`-f`) is passed (same guard as `migrate fresh`), and `--force` / `-f` is the only argument it accepts. Destructive. Outside production there is 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` | | `3` | Max attempts per job before marking failed | | `--timeout` | | `30` | Per-job timeout in seconds | `--tries` and `--timeout` require integer values; when omitted the worker's own defaults apply. Worker errors are routed through the application logger. `SIGINT` / `SIGTERM` stops the worker gracefully. ```bash vel queue work vel queue work --queue emails --tries 3 --timeout 60 ``` ### vel schedule work Run the scheduler loop - picks up scheduled tasks defined via `v.Schedule(...)` and dispatches them when due. ```bash vel schedule work ``` Takes no arguments. Typically run under a process supervisor (systemd, Docker, etc.) rather than manually; `SIGINT` / `SIGTERM` shuts it down gracefully. ## Cache ### vel cache clear Flush the configured cache store. ```bash vel cache clear ``` Takes no arguments. With no cache configured it warns and exits cleanly. ## Maintenance Mode ### vel down Put the app into maintenance mode. Requests return a `503` JSON response unless they carry the bypass secret (or hit a path the maintenance middleware is configured to exclude, such as health probes and webhooks). ```bash vel down [--secret TOKEN] [--retry N] ``` | Flag | Default | Description | | ---------- | ------- | ------------------------------------------------------ | | `--secret` | (none) | Bypass token. Send it in the `X-Maintenance-Bypass` header (or visit `/`) to mint a signed `velocity_maintenance_bypass` cookie that exempts the browser for 12 hours | | `--retry` | (none) | Recorded as `retry_after` in the maintenance marker | ```bash vel down --secret "abc123" --retry 60 ``` The command writes a `.vel/down` marker file holding the secret, the retry value, and a UTC timestamp. Both the file (`0600`) and its directory (`0700`) are owner-only because the marker carries the bypass secret. The location is resolved independently of the current working directory and can be moved with `VELOCITY_MAINTENANCE_ROOT`, so the writer and the runtime middleware always agree on one path. Prefer the `X-Maintenance-Bypass` header over the legacy `/` path: a secret in the URL leaks into access logs, proxy logs, `Referer` headers, and browser history. ### vel up Exit maintenance mode by removing the marker file. ```bash vel up ``` Takes no arguments. A missing marker is not an error. ## Keys ### vel key generate Generate a fresh 32-byte encryption key and write it to `.env` under `APP_KEY`, base64-encoded with a `base64:` prefix. If `.env` does not exist it's created; an existing `APP_KEY=` line is replaced in place, and a file without one gets the key prepended as its first line. The `.env` file is written with owner-only (`0600`) permissions. ```bash vel key generate ``` Takes no arguments. ## Routes ### vel routes Print every registered route with method, path, and name. ```bash vel routes ``` Takes no arguments. Runs the bootstrap lifecycle internally before printing - the output always reflects the current `v.Routes(...)` definition. ## Code Generation All generators live under `gen `. Each scaffolds a file into the conventional location for that type, converting the name to the right case for the artifact (snake_case for file names, PascalCase for types). Every `gen` command accepts a `--dir ` flag to override the default output directory (the artifact's conventional folder). The value must be project-relative, is cleaned, and is rejected if it escapes the project tree or routes through a symlink. Existing files are never overwritten. Names are normalised before use: the artifact's own kind suffix is stripped and the rest is PascalCased, so `vel gen policy PostPolicy` and `vel gen policy Post` both write `internal/policies/post.go` holding `type PostPolicy`. File names are the snake_case form of the normalised name. Passing nothing but the suffix (for example `vel gen module Module`) errors rather than writing a file named `.go`. ### vel gen handler ```bash vel gen handler [--resource] [--api] [--dir PATH] ``` | Flag | Short | Default | Description | | ------------ | ----- | ------- | ------------------------------------------------ | | `--resource` | `-r` | off | Scaffold CRUD handlers (Index/Create/Store/Show/Edit/Update/Destroy) | | `--api` | | off | JSON responses instead of string/view responses | | `--dir` | | `internal/handlers` | Output root override | Output: `internal/handlers/.go`, holding `func Index(ctx *router.Context) error`-style functions. Namespaced names like `Admin/Dashboard` nest under the output root, with the package taken from the parent segment. ```bash vel gen handler User vel gen handler Post --resource vel gen handler Admin/Dashboard vel gen handler Product --api --resource vel gen handler User --dir internal/web/handlers ``` ### vel gen model ```bash vel gen model [--uuid] [--soft-deletes] [--migration] [--dir PATH] ``` | 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 create migration | | `--dir` | | `internal/models` | Output directory override | Output: `internal/models/.go`. The model embeds `orm.Model[T]`, `orm.UUIDModel[T]`, `orm.SoftDeleteModel[T]`, or `orm.SoftDeleteUUIDModel[T]` depending on the flags, declares `TableName()` (pluralised snake_case), and ships a commented-out `AssignableFields()` allowlist - mass assignment is deny-by-default, so fill it in (or declare `ProtectedFields()`) before writing to the model from a map. With `--migration`, a `create_` migration is generated with the same `--uuid` / `--soft-deletes` settings. That migration always lands in `database/migrations`; `--dir` applies to the model file only. ### vel gen migration ```bash vel gen migration [--create TABLE] [--table TABLE] [--uuid] [--soft-deletes] [--dir PATH] ``` | 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 | | `--dir` | `=VALUE` or space | Output directory override (default `database/migrations`) | Table names passed to `--create` / `--table` must match `[A-Za-z_][A-Za-z0-9_]*`. Output: `database/migrations/_.go`. The timestamp has second resolution. When a file with that version already exists, the generator walks the version forward a second at a time so two migrations scaffolded back to back cannot collide. ```bash vel gen migration create_posts --create=posts vel gen migration add_slug_to_posts --table=posts ``` ### Other gen commands All take a name argument and scaffold a file into the conventional directory. Each also accepts `--dir ` to override that directory, and takes no other flags. | Command | Output path | Generated symbol | | ------------------------------------ | ------------------------------------------- | -------------------------------------------------- | | `vel gen middleware RateLimit` | `internal/middleware/rate_limit.go` | `func RateLimit(next router.HandlerFunc) router.HandlerFunc` | | `vel gen event UserRegistered` | `internal/events/user_registered.go` | `type UserRegistered`, `Name()` returns `user.registered` | | `vel gen listener SendWelcomeEmail` | `internal/listeners/send_welcome_email.go` | `func SendWelcomeEmail(event interface{}) error` | | `vel gen job ProcessPayment` | `internal/jobs/process_payment.go` | `type ProcessPayment` (`Handle` / `Failed` / `MaxAttempts`) | | `vel gen mail OrderShipped` | `internal/mail/order_shipped.go` | `type OrderShipped` (`Envelope` / `Content`) | | `vel gen notification InvoicePaid` | `internal/notifications/invoice_paid.go` | `type InvoicePaid` (`Via` / `ToMail`) | | `vel gen resource Post` | `internal/resources/post.go` | `type PostResource` | | `vel gen policy Post` | `internal/policies/post.go` | `type PostPolicy` | | `vel gen module Billing` | `internal/modules/billing.go` | `type BillingModule` | | `vel gen command SyncInventory` | `internal/commands/sync_inventory.go` | `type SyncInventoryCommand` | `vel gen module` writes a `Module` type in package `modules` with the full module lifecycle already stubbed: ```go func (m *BillingModule) Init(s *velocity.Services) error { return nil } func (m *BillingModule) Start(s *velocity.Services) error { return nil } func (m *BillingModule) Shutdown(ctx context.Context) error { return nil } ``` `vel gen command` writes a custom command implementing `Name()`, `Description()`, and `Handle(s *velocity.Services, args []string) error`, with the invocation name derived in kebab-case (`SyncInventory` becomes `sync-inventory`). The generated file carries its own registration hint (`r.Add(&SyncInventoryCommand{})`); once registered it runs through [`vel run`](#vel-run). ### vel gen grpc service ```bash vel gen grpc service [flags] ``` 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 a `NewService()` constructor and the `.UnimplementedServiceServer` embed - `internal/modules/grpc_module.go` - created on first call (unless `--no-module`), then **injected at** `// vel:grpc:imports` and `// vel:grpc:services` markers on every subsequent call. The module wires the service via `velgrpc.NewServer(...)` and `RegisterServiceServer(...)`. | Flag | Default | Description | | ----------------- | --------------------------- | ------------------------------------------------------------ | | `--package` | derived from `` | Directory leaf under `api/proto/` and `api/gen/go/` | | `--proto-package` | `.v1` | Full wire package, e.g. `velship.admin.v1` | | `--dir` | `internal/grpc/services` | Go impl output directory | | `--alias` | `pb` | Import alias for the generated proto package | | `--proto-name` | lower-cased `` base | Proto file base name (no extension) | | `--impl-name` | snake_case `` base | Go impl file base name (no extension) | | `--no-module` | off | Skip module scaffolding / wiring (proto + impl only) | Name normalisation: `vel gen grpc service Foo`, `FooService`, `foo`, and `fooService` all produce the Go type `FooService` with proto package `foo.v1` and default import alias `foopb`. The proto file uses `option go_package = "/api/gen/go//v1;v1"` derived from the host project's `go.mod` (so the generated package itself is named `v1`, referenced through the `foopb` alias). `buf.yaml` / `buf.gen.yaml` are written **before** the proto file, so a config-write failure leaves no partial scaffold on disk. The generated `GRPCModule` does **not** hard-code `WithReflection(true)`; it reads `GRPC_PORT` (default `50051`) and otherwise takes the framework defaults, including reflection off unless `GRPC_REFLECTION=true`. Wiring guards: - If `internal/modules/grpc_module.go` already exists **without** the marker comments (legacy hand-written module), the command prints a manual wire snippet instead of mutating user code. - If the existing module imports a services package other than this service's impl directory, the command stops **before** writing any file and tells you to re-run with `--no-module` and wire it by hand. - When the module already imports the generated proto package under a different alias, that alias is reused rather than emitting a duplicate import. ```bash vel gen grpc service Foo vel gen grpc service ChatService vel gen grpc service TemplateControl --package admin \ --proto-package velship.admin.v1 --dir internal/shared/grpc/services --no-module ``` After scaffolding, register `&modules.GRPCModule{}` in `internal/app/bootstrap.go` (printed as a hint on first run). ### vel gen grpc rpc ```bash vel gen 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 gen grpc service ` first. Both paths are derived from the service name alone, matching what `vel gen grpc service` writes by default: `api/proto//v1/.proto` (lower-cased, no underscores) and `internal/grpc/services/.go` (snake_case). A service scaffolded with `--package`, `--dir`, `--proto-name`, or `--impl-name` is not found under those paths and has to be extended by hand; the error message prints the exact path that was expected. | 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 (for service `Foo`, impl type `FooService`, default proto alias `foopb`): | Shape | Signature | | ------------- | ----------------------------------------------------------------------------------------------- | | Unary | `func (s *FooService) X(ctx context.Context, req *foopb.XRequest) (*foopb.XResponse, error)` | | Server stream | `func (s *FooService) X(req *foopb.XRequest, stream foopb.FooService_XServer) error` | | Client stream | `func (s *FooService) X(stream foopb.FooService_XServer) error` | | Bidi | `func (s *FooService) X(stream foopb.FooService_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 gen grpc rpc Foo Hello vel gen grpc rpc Foo Tail --stream vel gen grpc rpc Foo Upload --client-stream vel gen grpc rpc Foo Chat --bidi ``` ### vel gen grpc gen ```bash vel gen 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. Takes no arguments, and fails with a clear message when: - `api/proto/` does not exist (run `vel gen grpc service ` first) - `buf` is not on `PATH` (links to install docs) - `buf generate` exits non-zero ```bash vel gen grpc gen # cd api/proto && buf generate # Generated Go code in api/gen/go/ ``` ## Custom Commands ### vel run Run a command your application registered. ```bash vel run [arguments] ``` Everything after the command name is passed straight through to the command's `Handle(s *velocity.Services, args []string) error`, so `vel run seed --fresh` reaches your code with `["--fresh"]`. ```bash vel run # list every registered command vel run seed vel run seed --fresh ``` `vel run` with no arguments prints the registered commands (or a hint to create one with `vel gen command `). An unknown name prints the same list and errors, and a flag-like first token (`vel run --bogus`) is rejected as an unknown flag before the app bootstraps. Custom commands are reachable only through `vel run`: typing one as a bare `vel ` is an unknown command, so a command sharing a name with a built-in never shadows it. ## Help ```bash vel help vel --help vel -h ``` Prints a grouped list of every command: Server, Database, Queue & Scheduler, Cache, Code Generation, Custom Commands, and Other. Running `vel` with no arguments prints the same listing. ================================================================================ # Configuration Source: https://vel.build/docs/cli/configuration/ Section: CLI Summary: Configure a Velocity application through environment variables and the .env file, or override settings in code with the Config struct and functional options. A Velocity application is configured entirely through **environment variables**, loaded once at startup. There is no separate config store or CLI to manage settings. Every value comes from the process environment (or a `.env` file) and is read into a typed `Config` struct. ## How Configuration Is Loaded When you call `velocity.New()`, the framework loads configuration with `ConfigFromEnv()`. This reads a `.env` file from the working directory (if present) and then reads each setting from the environment, applying built-in defaults where a variable is unset. ```go package main import ( "log" "github.com/velocitykode/velocity" ) func main() { v, err := velocity.New() // loads ConfigFromEnv() internally if err != nil { log.Fatal(err) } if err := v.Serve(); err != nil { log.Fatal(err) } } ``` If a `.env` file exists but fails to parse, Velocity logs a warning and continues with the process environment. A missing `.env` file is not an error: environment variables alone are sufficient. ## The .env File Place a `.env` file at the root of your project. Each line sets one environment variable: ```bash # .env APP_ENV=production APP_DEBUG=false APP_PORT=4000 APP_KEY=base64:... DB_CONNECTION=postgres DB_HOST=127.0.0.1 DB_DATABASE=myapp DB_USERNAME=myapp DB_PASSWORD=secret CACHE_DRIVER=redis QUEUE_DRIVER=redis ``` ## Common Settings The most frequently used variables and their defaults: | Variable | Description | Default | |----------|-------------|---------| | `APP_ENV` | Application environment | _(empty)_ | | `APP_DEBUG` | Enable debug output | `false` | | `APP_PORT` | HTTP server port | `4000` | | `APP_KEY` | Application key used for crypto | _(empty)_ | | `DB_CONNECTION` | Database driver (`sqlite`, `postgres`, `mysql`) | _(empty)_ | | `DB_HOST` | Database host | `127.0.0.1` | | `DB_PORT` | Database port | _(per driver)_ | | `DB_DATABASE` | Database name | _(empty)_ | | `DB_USERNAME` | Database username | _(empty)_ | | `DB_PASSWORD` | Database password | _(empty)_ | | `CACHE_DRIVER` | Cache driver (`memory`, `file`, `redis`, `database`) | `memory` | | `QUEUE_DRIVER` | Queue driver (`memory`, `redis`, `database`) | `memory` | | `STORAGE_DRIVER` | Default storage disk | `local` | | `LOG_DRIVER` | Log driver | `console` | | `LOG_LEVEL` | Minimum log level | `debug` | `APP_ENV` is intentionally empty when unset. Security gates fail closed for an unknown environment, so production deployments that forget to set `APP_ENV` do not inherit development relaxations. ## Overriding Configuration in Code For tests or single-binary deployments you can bypass the environment and pass configuration directly. `velocity.New` accepts functional options. ### WithConfig `WithConfig` supplies a fully-built `Config` struct, replacing `ConfigFromEnv()`: ```go cfg := velocity.ConfigFromEnv() cfg.Port = "8080" cfg.Debug = true v, err := velocity.New(velocity.WithConfig(cfg)) ``` ### Targeted Options Smaller overrides have dedicated options: ```go v, err := velocity.New( velocity.WithPort("8080"), velocity.WithReadTimeout(15*time.Second), velocity.WithWriteTimeout(15*time.Second), velocity.WithIdleTimeout(60*time.Second), ) ``` Other options include `velocity.WithModules(...)` to append modules, `velocity.WithSchedulerInProcess()` to run the scheduler inside the HTTP process, and `velocity.WithoutEvents()` / `velocity.WithFakeEvents(...)` for tests. ## Priority Order Configuration values are resolved in this order: 1. **Functional options** passed to `velocity.New(...)` (highest priority, applied after the base config is built) 2. **Environment variables** (including those loaded from `.env`) 3. **Built-in defaults** (lowest priority) ```go // DB_DATABASE from the environment is used as the base, // but this code forces the port regardless of APP_PORT. v, err := velocity.New(velocity.WithPort("9000")) ``` ## Server Timeouts HTTP server timeouts can be set through the environment or with the matching options: | Variable | Option | Default | |----------|--------|---------| | `SERVER_READ_TIMEOUT` | `WithReadTimeout` | `30s` | | `SERVER_WRITE_TIMEOUT` | `WithWriteTimeout` | `30s` | | `SERVER_IDLE_TIMEOUT` | `WithIdleTimeout` | `120s` | | `SERVER_READ_HEADER_TIMEOUT` | _(env only)_ | `10s` | ================================================================================ # 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`, `gen *`) 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` | | `--stack` | `react` | Frontend stack for full-stack projects: `react`, `vue` | | `--api` | `false` | API-only project (no frontend) | | `--ssr` | `false` | Enable Inertia SSR (sets `VIEW_SSR_ENABLED=true`, wires Vite SSR) | | `-y`, `--non-interactive`| `false` | Skip all prompts; use flag values or defaults | ```bash velocity new myapp velocity new myapp --database postgres --cache redis velocity new myapp --stack vue velocity new myapi --api --database postgres velocity new myapp --ssr velocity new myapp -y --database postgres # no prompts ``` ### Interactive prompts Run without `-y`/`--non-interactive`, any flag you don't pass is asked interactively: project type (full stack vs API only), database, cache, frontend stack (full-stack only), and SSR. A flag you _do_ pass is taken as-is and not prompted. Pass `-y` (or set every relevant flag) to skip prompts entirely - useful for scripts and CI. ### What `new` does 1. Fails fast if the destination path already exists. 2. Downloads the appropriate starter template as a tarball from GitHub (`velocity-template-react`, `velocity-template-vue`, or `velocity-template-api`), resolved live to that template's newest released tag (falling back to `main` when no tag is found). Falls back to `git clone` if the tarball fetch fails. 3. Rewrites the Go module name to match the project, and strips any local `replace` directive for the framework from `go.mod`. 4. Re-initialises git (removes the template's history, creates a fresh repo with an initial commit). 5. Creates default migrations when the template doesn't ship its own. 6. Writes `.env` from `.env.example` and seeds freshly generated `APP_KEY`, `QUEUE_SIGNING_KEY`, and `AUTH_JWT_SECRET`, then patches the chosen database, cache, and SSR settings. 7. Installs Go dependencies (`go mod tidy`) - plus JS dependencies (`bun install`, or `npm install` when bun is absent) on full-stack projects - and builds the project's `vel` binary concurrently. 8. Checks the database is reachable, then runs the initial migrations. If the database isn't ready, scaffolding still completes and the installer prints the remaining steps (`./vel migrate`, `./vel serve`) instead of failing. When it finishes, the installer prints the next steps rather than starting a server. `cd` into the project and run `./vel serve` (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 or Vue) | None | | CSRF | Enabled | Disabled (stateless) | | Auth scheme | `web` (`AUTH_SCHEME=web`) | `api` (`AUTH_SCHEME=api`) | | Responses | Inertia-rendered pages | JSON (`ensure_json` middleware) | | Starter routes | `/`, `/login`, `/register`, `/dashboard`, `/logout`, `/health` | `/health`, `/api/health` | The `--stack` flag (`react` or `vue`) only applies to full-stack projects; it's ignored with `--api`. You can't flip between modes after scaffolding - choose up front. ## velocity config Manage global CLI defaults stored on disk. ```bash velocity config set velocity config get velocity config list velocity config reset ``` ### Keys Every key is unset until you set it - there are no seeded defaults. `set` validates the value against the accepted set and rejects unknown keys. | Key | Accepted values | | ------------------- | -------------------------------- | | `default.database` | `postgres`, `mysql`, `sqlite` | | `default.cache` | `redis`, `memory` | | `default.queue` | `redis`, `database` | | `default.auth` | `true`, `false` | | `default.api` | `true`, `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 set values velocity config reset # delete the config file ``` `get` prints the value, or `(not set)` for keys that are empty or `false`. `list` shows only keys that have been set. Configuration is stored at `~/.vel/config.yaml` (created with `0600` permissions, written under a file lock so concurrent runs don't clobber each other). ## velocity self-update Fetch and install the latest installer release. ```bash velocity self-update ``` It checks the latest GitHub release, and if you're already on it, reports "Already up to date" and stops. Otherwise it downloads the archive for your OS/architecture, verifies it against the release `checksums.txt`, extracts the binary, and atomically replaces the running executable in place. On macOS it also clears the download quarantine attribute. If the installer was installed via Homebrew, `self-update` detects this and declines, pointing you to `brew upgrade --cask velocity` instead - let the package manager own the binary it installed. ## velocity --version Print the installer version followed by the template tags it would scaffold. Each stack resolves to its newest released tag (or `main` when no tag can be resolved), so the output reflects the exact build coordinates of a fresh project: ```bash velocity --version ``` ``` velocity 0.21.14 templates: api -> v0.x.y react -> v0.x.y vue -> v0.x.y ``` ## 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 it prints one of the following and exits: ``` Go is not installed or not in PATH. Velocity requires Go 1.26 or higher. Please install Go: brew install go Or download from: https://go.dev/dl/ ``` ``` Go version go1.24.2 is not supported. Velocity requires Go 1.26 or higher. Please upgrade Go: brew upgrade go Or download from: https://go.dev/dl/ ``` Install or upgrade Go and run the command again. ================================================================================ # Configuration Source: https://vel.build/docs/core/config/ Section: Core Framework Summary: Manage application configuration with environment variables and the structured velocity.Config struct in Velocity. Velocity provides a simple yet powerful configuration system that reads from environment variables and a `.env` file, then exposes a single strongly-typed `velocity.Config` struct that every framework package consumes. ## Quick Start **Environment-Based**: Velocity loads configuration from environment variables and an optional `.env` file, following the twelve-factor app methodology. ```go import "github.com/velocitykode/velocity" func main() { // velocity.New loads ConfigFromEnv() by default: // it reads .env (if present) and the process environment. app, err := velocity.New() if err != nil { panic(err) } if err := app.Serve(); err != nil { panic(err) } } ``` ```env # .env file APP_ENV=production APP_DEBUG=false APP_PORT=4000 # Database DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=myapp DB_USERNAME=root DB_PASSWORD=secret # Cache CACHE_DRIVER=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 ``` ```go import "github.com/velocitykode/velocity" func main() { // Build the config explicitly, then override New's env-loaded default. cfg := velocity.ConfigFromEnv() cfg.Port = "8080" app, err := velocity.New(velocity.WithConfig(cfg)) if err != nil { panic(err) } _ = app } ``` ## How Configuration Loads `velocity.New()` calls `ConfigFromEnv()` automatically, so a default app reads its configuration from the environment with no extra wiring. The loader: 1. Calls `godotenv.Load()` to read a `.env` file from the working directory (if present). A `.env` that exists but fails to parse is logged as a warning, not a fatal error. 2. Reads every documented environment variable, applying defaults for unset values. 3. Returns a `velocity.Config` whose typed sub-structs (`DB`, `Cache`, `Queue`, `Storage`, `Session`, `Auth`, `CSRF`, `Crypto`, `Mail`, `View`, `Log`) are consumed by the matching framework packages. ```go import "github.com/velocitykode/velocity" func main() { cfg := velocity.ConfigFromEnv() // cfg.Env is the normalized (lowercased, trimmed) APP_ENV value. if cfg.Env == "production" && cfg.Debug { panic("APP_DEBUG must be false in production") } } ``` You rarely need to call `ConfigFromEnv()` yourself. Use it only when you want to inspect or mutate the config before passing it to `velocity.New` via `WithConfig`. ## The Config Struct `velocity.Config` is the single source of truth for application configuration. The top-level fields and their backing environment variables are: ```go type Config struct { // App Env string // APP_ENV, empty when unset Debug bool // APP_DEBUG, default false Port string // APP_PORT, default "4000" Key string // APP_KEY (used for crypto) DB DBConfig // DB_* Auth auth.Config // AUTH_* Cache CacheConfig // CACHE_*, REDIS_* Log log.LogConfig // LOG_* Queue QueueConfig // QUEUE_* Storage StorageConfig // STORAGE_*, FILESYSTEM_*, AWS_* CSRF csrf.Config // CSRF_* Session auth.SessionConfig // SESSION_* View view.Config // VIEW_SSR_* Crypto crypto.Config // CRYPTO_* Mail mail.MailConfig // MAIL_* // Server timeouts ReadTimeout time.Duration // SERVER_READ_TIMEOUT, default 30s WriteTimeout time.Duration // SERVER_WRITE_TIMEOUT, default 30s IdleTimeout time.Duration // SERVER_IDLE_TIMEOUT, default 120s ReadHeaderTimeout time.Duration // SERVER_READ_HEADER_TIMEOUT, default 10s // FileRoot bounds Context.File / Context.Download / Context.SaveFile. // Sourced from FILE_ROOT; defaults to the process working directory. FileRoot string } ``` ### Database Configuration ```go type DBConfig struct { Connection string // DB_CONNECTION: sqlite, postgres, mysql Host string // DB_HOST, default "127.0.0.1" Port string // DB_PORT, default per driver (mysql 3306, postgres 5432) Database string // DB_DATABASE Username string // DB_USERNAME Password string // DB_PASSWORD Charset string // DB_CHARSET SSLMode string // DB_SSL_MODE (postgres) TLS string // DB_MYSQL_TLS (true/false/skip-verify/preferred) MaxIdleConns int // DB_MAX_IDLE_CONNS, default 10 MaxOpenConns int // DB_MAX_OPEN_CONNS, default 100 ConnMaxLifetime time.Duration // DB_CONN_MAX_LIFETIME (seconds), default 3600 LogQueries bool // DB_LOG_QUERIES SlowThreshold time.Duration // DB_SLOW_QUERY_THRESHOLD } ``` ### Cache Configuration ```go type CacheConfig struct { Driver string // CACHE_DRIVER: memory, file, redis, database (default "memory") Prefix string // CACHE_PREFIX, default "velocity_cache" Path string // CACHE_PATH (required when CACHE_DRIVER=file) MemoryMaxEntries int // CACHE_MEMORY_MAX_ENTRIES (0 = 1,000,000, negative = unlimited) MaxValueBytes int64 // CACHE_MAX_VALUE_BYTES (0 = unlimited) RedisHost string // REDIS_HOST, default "127.0.0.1" RedisPort int // REDIS_PORT, default 6379 RedisPassword string // REDIS_PASSWORD RedisDatabase int // REDIS_DATABASE, default 0 RedisTLS bool // REDIS_TLS } ``` ### Queue Configuration ```go type QueueConfig struct { Driver string // QUEUE_DRIVER: memory, redis, database (default "memory") RedisHost string // QUEUE_REDIS_HOST, default "localhost" RedisPort string // QUEUE_REDIS_PORT, default "6379" RedisPassword string // QUEUE_REDIS_PASSWORD RedisDB string // QUEUE_REDIS_DB, default "0" RedisTLS bool // REDIS_TLS SigningKey string // QUEUE_SIGNING_KEY: HMAC key for payload signing Encrypt bool // QUEUE_ENCRYPT: encrypt job payloads at rest } ``` ### Storage Configuration ```go type StorageConfig struct { Default string // STORAGE_DRIVER, default "local" Disks map[string]DiskConfig // configured disks } type DiskConfig struct { Driver string // "local", "s3", "memory" Root string // root path for the local driver URL string // base URL for file access Visibility string // default visibility (public/private) Bucket string // s3 Region string // s3 Key string // s3 Secret string // s3 MaxSize int64 // memory driver max bytes } ``` A `local` disk is always configured (root from `FILESYSTEM_LOCAL_ROOT`, default `./storage/app`). An `s3` disk is added automatically when `AWS_BUCKET` is set, reading `AWS_DEFAULT_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_URL`. ## Configuration Options Pass `velocity.Option` functions to `velocity.New` to override the env-loaded config. ```go import ( "time" "github.com/velocitykode/velocity" ) app, err := velocity.New( velocity.WithPort("8080"), velocity.WithReadTimeout(15*time.Second), velocity.WithWriteTimeout(15*time.Second), velocity.WithIdleTimeout(60*time.Second), ) ``` Available options include: | Option | Effect | | --- | --- | | `WithConfig(cfg Config)` | Replace the entire configuration. | | `WithPort(port string)` | Set the HTTP server port. | | `WithReadTimeout(d time.Duration)` | Set the HTTP read timeout. | | `WithWriteTimeout(d time.Duration)` | Set the HTTP write timeout. | | `WithIdleTimeout(d time.Duration)` | Set the HTTP idle timeout. | | `WithModules(modules ...app.Module)` | Append modules; they are initialized and started in the order given. | | `WithoutEvents()` | Disable the event dispatcher entirely. | | `WithFakeEvents(fake *events.FakeDispatcher)` | Record dispatched events for assertions. | | `WithSchedulerInProcess()` | Run the scheduler loop in the same process as `Serve()`. | ## Validating Configuration `velocity.New` calls `Config.Validate()` before allocating any resources, so malformed values (an invalid `APP_PORT`, a negative timeout, an unknown `SESSION_SAME_SITE`, a `file` cache driver without `CACHE_PATH`) fail fast with a clear error. You can also call it yourself: ```go import ( "errors" "github.com/velocitykode/velocity" ) cfg := velocity.ConfigFromEnv() if err := cfg.Validate(); err != nil { if errors.Is(err, velocity.ErrInvalidConfig) { // configuration is structurally invalid } return err } ``` `Validate()` performs structural checks only (port is numeric, timeouts are non-negative, `SESSION_SAME_SITE` / `CSRF_SAME_SITE` are one of `strict|lax|none`, and each sub-config's `Validate()` passes). It deliberately does not check driver names against an allowlist: an unknown driver surfaces as a typed registry error when the relevant subsystem resolves it. Every failure wraps the `velocity.ErrInvalidConfig` sentinel so callers can branch with `errors.Is`. ## Logging Configuration The root config stores logging settings in `Config.Log`, a `log.LogConfig`: ```go import "github.com/velocitykode/velocity/log" type LogConfig struct { // Driver: "console", "file", "stack", "null", or a registered driver. Driver string // Config holds driver-specific options, e.g. "path" for file, // "level" for any driver, "stack" with a []string of channel names. Config map[string]any } ``` `ConfigFromEnv()` populates it from `LOG_DRIVER` (default `console`), `LOG_PATH`, `LOG_LEVEL` (default `debug`), `LOG_DAYS` (default 14), and `LOG_STACK` (a comma-separated list of channel names for the `stack` driver). The `log` package also exposes a multi-channel `LoggingConfig` for applications that define named channels: ```go import "github.com/velocitykode/velocity/log" cfg := log.LoggingConfig{ Default: "stack", Channels: map[string]log.ChannelConfig{ "daily": {Driver: "file", Level: "debug", Path: "./storage/logs", MaxAge: 14}, }, } if channel, ok := cfg.GetChannel("daily"); ok { _ = channel.Driver } if channel, ok := cfg.GetDefaultChannel(); ok { _ = channel.Driver } ``` ```go type ChannelConfig struct { Driver string // file, console, syslog, null Level string // debug, info, warn, error Path string // file path (for file driver) MaxAge int // max age in days Options map[string]any // driver-specific options } ``` ## Environment Variables Reference A representative `.env` covering the most common settings: ```env # Application APP_ENV=production APP_DEBUG=false APP_PORT=4000 APP_KEY=base64:your-32-byte-base64-encoded-key # Crypto CRYPTO_KEY=base64:your-32-byte-base64-encoded-key CRYPTO_CIPHER=AES-256-GCM # Database DB_CONNECTION=mysql DB_HOST=127.0.0.1 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 LOG_DAYS=14 # 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_NAME=velocity_session SESSION_LIFETIME=120 SESSION_SECURE=true SESSION_HTTP_ONLY=true SESSION_SAME_SITE=lax # Server timeouts (accepts Go duration syntax, e.g. 30s) SERVER_READ_TIMEOUT=30s SERVER_WRITE_TIMEOUT=30s SERVER_IDLE_TIMEOUT=120s ``` **Defaults that differ from common expectations**: `MAIL_DRIVER` defaults to `log` (captured, not sent), `CRYPTO_CIPHER` defaults to `AES-256-GCM`, `CACHE_DRIVER` and `QUEUE_DRIVER` default to `memory`, `SESSION_SECURE` defaults to `true` (only the literal `false` disables it), and `DB_HOST` / `REDIS_HOST` default to `127.0.0.1`. ## Environment-Specific Configuration Velocity classifies `APP_ENV` through the canonical reader, so security gates relax only when you explicitly opt into a non-production profile. An unset `APP_ENV` is treated as production (fail-secure). ### Development ```env APP_ENV=development APP_DEBUG=true APP_PORT=4000 LOG_LEVEL=debug LOG_DRIVER=console CACHE_DRIVER=memory QUEUE_DRIVER=memory DB_HOST=127.0.0.1 DB_DATABASE=myapp_dev ``` ### Production ```env APP_ENV=production APP_DEBUG=false LOG_LEVEL=info LOG_DRIVER=file CACHE_DRIVER=redis QUEUE_DRIVER=redis DB_HOST=db.example.com DB_DATABASE=myapp_prod ``` ### Testing ```env APP_ENV=testing APP_DEBUG=true LOG_LEVEL=error LOG_DRIVER=null CACHE_DRIVER=memory QUEUE_DRIVER=memory DB_CONNECTION=sqlite DB_DATABASE=:memory: ``` ## Security Best Practices ### Sensitive Data Never commit secrets to version control: ```bash # .gitignore .env .env.local .env.production .env.*.local ``` ### Environment Template Provide a template for required variables: ```env # .env.example APP_ENV=development APP_DEBUG=true APP_PORT=4000 APP_KEY= # Database (required) DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE= DB_USERNAME= DB_PASSWORD= # Crypto (required for production) CRYPTO_KEY= ``` ## Testing Configuration In tests, build a `velocity.Config` directly and pass it with `WithConfig` rather than relying on the environment: ```go import ( "testing" "github.com/velocitykode/velocity" "github.com/velocitykode/velocity/log" "github.com/velocitykode/velocity/mail" ) func TestApp(t *testing.T) { app, err := velocity.New(velocity.WithConfig(velocity.Config{ Env: "testing", Debug: true, Port: "0", Cache: velocity.CacheConfig{Driver: "memory", Prefix: "test_cache"}, Log: log.LogConfig{Driver: "null", Config: make(map[string]any)}, Queue: velocity.QueueConfig{Driver: "memory"}, Mail: mail.MailConfig{Driver: "log"}, })) if err != nil { t.Fatalf("New() error: %v", err) } _ = app } ``` You can also point the loader at a dedicated env file before constructing the app: ```go func TestMain(m *testing.M) { _ = godotenv.Load(".env.testing") os.Exit(m.Run()) } ``` ## Docker Integration ### Docker Compose ```yaml # docker-compose.yml services: app: build: . ports: - "4000:4000" environment: - APP_ENV=production - APP_DEBUG=false - APP_PORT=4000 - 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. **Let `New()` load config**: a plain `velocity.New()` reads `.env` and the environment for you. 2. **Override via options**: use `WithConfig` / `WithPort` / `WithReadTimeout` instead of mutating globals. 3. **Validate early**: `Config.Validate()` runs inside `New()`; call it yourself when building config manually. 4. **Provide defaults**: keep an `.env.example` documenting every variable. 5. **Environment-specific files**: use `.env`, `.env.testing`, etc., for different profiles. 6. **Security**: never commit `.env` files; set `APP_KEY` / `CRYPTO_KEY` in production. 7. **Fail-secure env**: leave `APP_ENV` unset only when you intend production-grade defaults. ================================================================================ # 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-GCM # Auth settings AUTH_SCHEME=web 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_SCHEME`, `HASH_BCRYPT_COST`, and the `SESSION_*` variables, builds an `auth.Manager`, installs an ORM-backed user store, and wires a `SessionScheme` against the encrypted-cookie store. No manual wiring is required for the common case. `AUTH_SCHEME` is load-bearing twice over: it names the default scheme, and a non-empty value is what makes `ConfigFromEnv` build the scheme configs at all. The map keys it builds are `web` and `session` (driver `session`) plus `api` and `jwt` (driver `jwt`, skipped when `AUTH_JWT_SECRET` is empty). Leave `AUTH_SCHEME` unset and the app boots with no schemes registered. If you need to construct the manager yourself (custom scheme, embedded use, tests), the underlying API is: ```go package main import ( "net/http" "github.com/velocitykode/velocity/auth" "github.com/velocitykode/velocity/auth/drivers/schemes" "github.com/velocitykode/velocity/auth/stores/ormauth" "github.com/velocitykode/velocity/crypto" "myapp/internal/models" ) func buildAuth(enc crypto.Encryptor) (*auth.Manager, error) { manager := auth.NewManager() // User store: ORM-backed lookup for the model you authenticate. userStore := ormauth.New[models.User](ormauth.WithHasher(manager.GetHasher())) if err := userStore.Validate(); err != nil { return nil, err } manager.SetUserStore(userStore) // Scheme: encrypted-cookie session store. sessionScheme, err := schemes.NewSessionScheme(userStore, auth.SessionConfig{ Name: "velocity_session", Lifetime: 120, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, }, enc) if err != nil { return nil, err } manager.RegisterScheme("web", sessionScheme) manager.SetDefaultScheme("web") return manager, nil } ``` `SetUserStore` installs the single canonical user store (under `auth.DefaultUserStoreName`, value `"default"`) and re-points every scheme already registered, so construction order does not matter. `RegisterUserStore(name, store)` is the code-level escape hatch for the uncommon app that authenticates two separate identity stores in one process, an admin panel colocated with a customer app, for example. Schemes are not notified in that case, so you hand the store to whichever scheme should use it yourself. There is no per-scheme user-store field in `auth.SchemeConfig`. 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 ``` Outside a request, in a module lifecycle hook, use `auth.FromServices(s)`. ### Choosing the auth model The model that authenticates is chosen in code, not in `.env`. The ORM resolves a table from a compile-time Go type, and Go cannot turn the string `"Admin"` into a type, so the model is a type parameter. Editing it is a compile error when you get it wrong, rather than a boot failure: ```go import "github.com/velocitykode/velocity" func (m *AppModule) Init(s *velocity.Services) error { return velocity.SetAuthModel[models.User](s) } ``` `velocity.SetAuthModel[T]` validates the model, inherits the auth manager's hasher (so the operator-configured bcrypt cost is preserved), and installs the resulting user store. It returns `velocity.ErrAuthNotConfigured` when the app has no auth manager, which means `velocity.New` built no schemes because `AUTH_SCHEME` was unset. A model whose columns differ from the defaults (`email`, `password`, `remember_token`) names them with the re-exported options: ```go func (m *AppModule) Init(s *velocity.Services) error { return velocity.SetAuthModel[models.Admin](s, velocity.WithAuthIdentifierColumn("username"), velocity.WithAuthPasswordColumn("pass_hash"), ) } ``` The full option set is `velocity.WithAuthIdentifierColumn`, `velocity.WithAuthPasswordColumn`, `velocity.WithAuthRememberTokenColumn`, and `velocity.WithAuthCredentialsKey` (the key read from the credentials map when it differs from the identifier column, e.g. a form posting `email` against a `users.username` column). All four are aliases of the `ormauth.With*` options, so application code never has to import the store package. If you want the store itself rather than an installed one, to hand it to a second scheme or to inspect it in a test, `velocity.ORMUserStore[T](opts...)` builds it without installing. The direct form is equivalent: ```go import ( "github.com/velocitykode/velocity" "github.com/velocitykode/velocity/auth" "github.com/velocitykode/velocity/auth/stores/ormauth" ) func (m *AuthModule) Start(s *velocity.Services) error { userStore := ormauth.New[models.Admin]( ormauth.WithIdentifierColumn("username"), ) if err := userStore.Validate(); err != nil { return err } manager := auth.FromServices(s) if manager == nil { return velocity.ErrAuthNotConfigured } manager.SetUserStore(userStore) return nil } ``` `Services.Auth` is typed as `contract.AuthManager`, which carries only `Allows` and `Authorize`, so reach the concrete `*auth.Manager` through `auth.FromServices(s)` before calling `SetUserStore`. Call it from a module's `Init` or `Start`. `velocity.New` has already built the schemes against the framework's built-in model, and installing a user store re-points every one of them, so ordering does not matter. An app that configures nothing gets `ormauth.New[ormauth.User]` against the `users` table, reproducing the column set the framework used to hardcode (`id`, `name`, `email`, `password`, `remember_token`). ### User Model Requirements A model is usable as an auth model when it either implements `auth.Authenticatable` itself (preferred, since it skips the reflection-based column mapping entirely) or exposes the identifier, password, and remember-token columns that `ormauth` maps onto that interface. It must also declare a mass-assignment policy that permits the remember-token column. The token is persisted through the ORM's map-based update path, which is deny-by-default: a model declaring no policy at all rejects every key, so remember-me would fail on first use. `Store.Validate` refuses at startup instead. ```go type User struct { orm.IDInt[User] Name string `orm:"column:name" json:"name"` Email string `orm:"column:email" json:"email"` Password string `orm:"column:password" json:"-"` RememberToken *string `orm:"column:remember_token" json:"-"` } // AssignableFields declares the mass-assignment allowlist. Without a // declared policy (AssignableFields, ProtectedFields, or AllowAllColumns) // the ORM denies every map-based write, including the remember-token update. func (User) AssignableFields() []string { return []string{"name", "email", "password", "remember_token"} } // 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, or "" when the column is NULL func (u *User) GetRememberToken() string { if u.RememberToken == nil { return "" } return *u.RememberToken } // SetRememberToken sets the remember token func (u *User) SetRememberToken(token string) { u.RememberToken = &token } ``` Two shape details are load-bearing. `RememberToken` is a `*string` because the column is nullable for users who have never used remember-me, and scanning SQL NULL into a plain `string` is a driver error. Composing `orm.IDInt[User]` rather than `orm.Model[User]` keeps the ORM from stamping `updated_at` on every remember-token rotation, which happens on every remember-me recall; use `orm.Model[User]` only when you actually want login traffic touching `users.updated_at`. ## 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, "/dashboard") } else { view.Render(ctx, "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, "/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 `auth.Authenticatable` is the contract every authenticated user satisfies: ```go // auth.Authenticatable (auth/auth.go) type Authenticatable interface { GetAuthIdentifier() interface{} GetAuthPassword() string GetRememberToken() string SetRememberToken(token string) } ``` Implementing it on your model directly is the preferred path (see [User Model Requirements](#user-model-requirements)). A model that does not implement it is still usable: `ormauth` maps the configured identifier, password, and remember-token columns onto the interface through ORM metadata, at the cost of reflection on the lookup path. ### Custom User Stores `auth.UserStore` is the interface behind user lookup. It threads a `context.Context` through every method that does I/O so a cancelled request (client disconnect, timeout middleware) aborts the lookup. Each I/O method comes in a pair: a `Ctx`-suffixed variant that does the real work, and a deprecated non-`Ctx` shim that delegates with `context.Background()`. Implement all six methods plus `ValidateCredentials` (pure CPU, so no `Ctx` variant): ```go // Implement auth.UserStore for custom user retrieval type CustomUserStore struct { db *sql.DB } func (s *CustomUserStore) FindByIDCtx(ctx context.Context, id interface{}) (auth.Authenticatable, error) { var user User err := s.db.QueryRowContext(ctx, "SELECT id, email, password, name FROM users WHERE id = $1", id). Scan(&user.ID, &user.Email, &user.Password, &user.Name) if err != nil { return nil, err } return &user, nil } // Deprecated: use FindByIDCtx with a request-scoped context.Context. func (s *CustomUserStore) FindByID(id interface{}) (auth.Authenticatable, error) { return s.FindByIDCtx(context.Background(), id) } func (s *CustomUserStore) FindByCredentialsCtx(ctx context.Context, credentials map[string]interface{}) (auth.Authenticatable, error) { email := credentials["email"].(string) var user User err := s.db.QueryRowContext(ctx, "SELECT id, email, password, name FROM users WHERE email = $1", email). Scan(&user.ID, &user.Email, &user.Password, &user.Name) if err != nil { return nil, err } return &user, nil } // Deprecated: use FindByCredentialsCtx with a request-scoped context.Context. func (s *CustomUserStore) FindByCredentials(credentials map[string]interface{}) (auth.Authenticatable, error) { return s.FindByCredentialsCtx(context.Background(), credentials) } func (s *CustomUserStore) ValidateCredentials(user auth.Authenticatable, credentials map[string]interface{}) bool { password, _ := credentials["password"].(string) return auth.NewBcryptHasher(10).Verify(password, user.GetAuthPassword()) } func (s *CustomUserStore) UpdateRememberTokenCtx(ctx context.Context, user auth.Authenticatable, token string) error { user.SetRememberToken(token) _, err := s.db.ExecContext(ctx, "UPDATE users SET remember_token = $1 WHERE id = $2", token, user.GetAuthIdentifier()) return err } // Deprecated: use UpdateRememberTokenCtx with a request-scoped context.Context. func (s *CustomUserStore) UpdateRememberToken(user auth.Authenticatable, token string) error { return s.UpdateRememberTokenCtx(context.Background(), user, token) } ``` Install it with `manager.SetUserStore(store)`, which fans out to every registered scheme. For atomic rotate-on-use of the remember-me credential, a user store may additionally implement `auth.RememberTokenCompareAndSwapper` (`CompareAndSwapRememberToken(ctx, user, oldToken, newToken) (swapped bool, err error)`); `SessionScheme` recall persists rotation exclusively through it. A user store that does not implement it fails remember-cookie recall closed: remember cookies are still issued at login, but can never revive a session. The interface has an executable specification. Run it against your implementation with `authtest.RunUserStoreContractTests` from `github.com/velocitykode/velocity/auth/authtest`: ```go func TestCustomUserStore(t *testing.T) { authtest.RunUserStoreContractTests(t, authtest.UserStoreFactory{ New: func(t *testing.T) auth.UserStore { return newSeededStore(t) }, SeedUser: seedUser, SeedEmail: "user@example.com", SeedPassword: "secret123", }) } ``` ## Middleware Integration ### Auth Middleware Use `auth.AuthMiddleware` to require authentication on a route. It returns 401 JSON for API requests and redirects HTML requests to a clean `/login` (303 See Other). The originally requested GET URL is stashed server-side in the session rather than exposed as a `?redirect=` query parameter, so it cannot be tampered with; `ctx.RedirectToIntended(fallback)` pulls it back after a successful login. ```go import "github.com/velocitykode/velocity/auth" r.Get("/dashboard", dashboardHandler.Index, auth.AuthMiddleware(manager)) ``` For role- or ability-based access checks, 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. They resolve through the manager's authorizer, `auth.Access`, reachable as `manager.Access()`; `manager.Allows(r, ability, args...)` and `manager.Authorize(r, ability, args...)` are the `contract.AuthManager` methods that wrap it for a request. ### 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{} FlushFlash() map[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 `SessionScheme` against it, and register that scheme with the manager. `SessionScheme` 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 scheme 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 Touch(ctx context.Context, id string, lastSeen time.Time) 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). `Put` is the login-time write only: it creates or replaces the record. The per-request activity refresh (the debounced `LastSeenAt` write the session scheme issues on every authenticated request) goes through `Touch`, which is update-if-present and returns `auth.ErrSessionNotFound` when the record is gone. That distinction is what makes revocation stick: if an administrator deletes the session between the scheme's read and its refresh write, `Touch` cannot recreate the row, and the scheme denies the request that lost the race. When you implement your own store, `Touch` must never insert, and your application code must not call `Put` to record activity. 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`. Two drivers ship in `auth/drivers/session`: - **`session.NewCacheStore(backend)`** is the production driver. It keeps the records in a velocity cache store, so every app instance sharing the same Redis sees the same sessions and a revocation issued on one replica is enforced on all of them. Its writes are atomic on the backend: the per-user index is a backend set (`contract.CacheSetStore`, Redis `SADD`/`SREM`), `Touch` goes through `contract.CacheReplacer` (Redis `SET XX`) so a refresh can never recreate a deleted record, and `DeleteAllForUser` rotates a per-user generation token before it touches the index, with `Get` rejecting any record issued under an older token. That last part is what makes "sign out everywhere" authoritative even if the index is incomplete. Every record is stamped with a token from `Put` on, and a token the backend cannot serve fails closed: the session is rejected, never assumed pre-revocation. The index expiry is extend-only, so a short-lived login after a long-lived one never shortens the listing's life. The backend must implement both capabilities; the memory and redis cache drivers do, the file driver does not, and `NewCacheStore` returns `session.ErrCacheStoreUnsupported` at boot rather than failing at the first revocation. - **`session.NewMemoryStore()`** is an in-process implementation 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. Wire one at bootstrap and the manager helpers light up: ```go import ( "github.com/velocitykode/velocity/auth" "github.com/velocitykode/velocity/auth/drivers/session" ) // Production: share the app's cache backend (CACHE_DRIVER=redis). backend, err := s.Cache.DefaultStore() if err != nil { return err } store, err := session.NewCacheStore(backend) if err != nil { return err } manager.SetServerSessionStore(store) // Development / tests: manager.SetServerSessionStore(session.NewMemoryStore()) ``` 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(http.StatusUnauthorized, "unauthorized") } // ... 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 `SessionScheme.Attempt` (and `JWTScheme.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 `scheme.SetLoginThrottler(yourThrottler)` (passing `nil` reverts to the no-op), or with `manager.SetLoginThrottler(yourThrottler)` to reach every scheme that implements `auth.LoginThrottlerReceiver`, including schemes registered later. The framework also exposes `auth.ThrottleKey(r, credentials, trustedProxies)`, which derives the rate-limit key for the `(identifier, IP)` pair dimension as a length-bounded SHA-256 digest prefixed with `login:`. The `identifier` is the first non-empty value among `email`, `username`, `name`, `login` in the credentials map (normalised: trimmed, NFKC-folded, lowercased), and the client IP is resolved through the trusted-proxy list (pass `nil` to ignore forwarded headers, the secure default). Use it so a custom scheme wrapper produces keys consistent with the built-in schemes. The built-in schemes actually consult `auth.ThrottleKeys(r, credentials, trustedProxies)`, which returns up to three keys, one per throttle dimension: the `(identifier, IP)` pair (always present, prefix `auth.ThrottleKeyPairPrefix`), the per-identifier key (prefix `auth.ThrottleKeyIdentifierPrefix`, omitted when no identifier is present), and the per-IP key (prefix `auth.ThrottleKeyIPPrefix`, omitted when the IP cannot be resolved). A throttler can branch on those prefixes to apply an independent cap per dimension. ## 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/