-
Notifications
You must be signed in to change notification settings - Fork 0
Quick Start
This guide walks you from zero to a working Goten-backed HTTP server with a real PostgreSQL database, migrations applied, and a verified sign-up / sign-in / protected-route flow.
If you just want to read code, jump straight to examples/basic/ in the repo — this guide drives that same example.
- Prerequisites
- Install the library
- Minimal
main.go - Run the bundled example
- Try it end-to-end with
curl - Next steps
-
Go 1.23+ —
go version -
Docker + Docker Compose — used to run the Postgres dev database. Any local Postgres works too; just point
DATABASE_URLat it. -
make(optional but recommended) — the repo ships aMakefilewith all-in-one targets likemake example.
You do not need to install Postgres on the host; the example uses a containerised Postgres 16.
For a new project, pull in core and the GORM adapter:
go get github.com/dnahilman/goten
go get github.com/dnahilman/goten/adapters/gormIf you want username-based login as well, also pull the username plugin:
go get github.com/dnahilman/goten/plugins/usernameEach module is independently versioned — go get only what you actually use.
The smallest useful integration looks like this:
package main
import (
"log"
"net/http"
goten "github.com/dnahilman/goten"
gormadapter "github.com/dnahilman/goten/adapters/gorm"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
db, err := gorm.Open(postgres.Open("postgres://goten:goten@localhost:5432/goten?sslmode=disable"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
auth, err := goten.New(goten.Config{
// BaseURL must match where the server is actually reached by the browser/client.
// It is used for cookie scoping and origin checks.
BaseURL: "http://localhost:8080",
// Secret must be at least 32 bytes. Generate one with: openssl rand -base64 32
Secret: "your-32-byte-secret-key-here!!!!",
Adapter: gormadapter.New(db),
})
if err != nil {
log.Fatal(err)
}
// Mount Goten's auth endpoints at /api/auth/*
http.Handle("/api/auth/", auth.Handler())
// Protect your own endpoints
http.Handle("/api/me", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, _ := goten.UserFromContext(r.Context())
_ = user // user.ID, user.Email, user.Name ...
})))
log.Fatal(http.ListenAndServe(":8080", nil))
}Two things that catch most people:
-
Secretmust be ≥ 32 bytes. Goten refuses to start with a shorter key. Use a real random value in production. -
BaseURLmust match the URL the client uses. A mismatch breaks cookie scoping and the CSRF origin check.
This snippet by itself is not enough — you still need a database with Goten's schema applied. The fastest way is to use the example below.
The repo ships a ready-to-run example at examples/basic/. It wires up the snippet above, adds the username plugin, and includes a Postgres docker-compose.yml.
The goten CLI (used for running migrations) reads a goten.config.yaml from the current working directory at startup. It is not used by the runtime server — only by the CLI.
database:
# Full Postgres DSN. The CLI connects here to apply or roll back migrations.
url: postgres://goten:goten@localhost:5432/goten?sslmode=disable
driver: postgres # only "postgres" is supported currently
migrations:
# Path to core migration SQL files, relative to this config file.
core_dir: ./migrations
# Paths to each plugin's migration directory (one entry per plugin).
plugins:
- ./plugins/username/migrations
# Name of the table Goten uses to track applied migrations.
# Defaults to "goten_migrations" if omitted.
table: goten_migrations
# Where `goten migrate generate <name>` writes new migration files.
generate_dir: ./migrationsPlace this file in the directory from which you run goten migrate .... In the bundled example it lives at examples/basic/goten.config.yaml and points at the migration directories relative to the repo root — that is why step 3 in Option B below says to cd examples/basic first.
You can also point the CLI at a different config file with the --config flag:
goten --config /path/to/goten.config.yaml migrate upDo not put real database passwords in goten.config.yaml and commit the file. There are two ways to keep them out.
Option 1 — ${VAR} interpolation in YAML (cleanest):
The CLI expands ${VAR} references against the current environment before parsing the YAML. Bare $VAR (no braces) is left untouched, so passwords containing literal $ are safe.
# goten.config.yaml — safe to commit, references env
database:
url: ${DATABASE_URL}
driver: postgres
migrations:
core_dir: ./migrations
plugins:
- ./plugins/username/migrations
table: goten_migrations
generate_dir: ./migrationsexport DATABASE_URL="postgres://user:realpass@prod-host:5432/mydb"
goten migrate up # clean — no inline secret in the commandAny field can be interpolated, not just database.url. If a ${VAR} is unset, it expands to an empty string and the usual database.url required validation catches the case for required fields.
The CLI auto-loads a .env file from the current working directory if one exists. Combined with ${VAR} interpolation, you do not need to export anything in the shell:
# .env — keep in .gitignore
DATABASE_URL=postgres://user:realpass@prod-host:5432/mydbgoten migrate up # .env auto-loaded, ${DATABASE_URL} in YAML resolvesFor non-default locations there are two equivalent options.
A) Declare it in goten.config.yaml (preferred — no flag needed on every call):
env_file: ./config/.env.staging
database:
url: ${DATABASE_URL}
driver: postgresgoten migrate up # picks up env_file from the configB) Pass it on the command line (Docker-style, useful for one-off overrides):
goten --env-file ./config/.env.staging migrate upPrecedence when both are set: --env-file flag > env_file in YAML > default .env in CWD.
Notes:
- A missing default
.envis silently ignored; a missing explicitenv_file(either flag or YAML) is a fatal error — explicit means you meant it. - Real environment variables always win —
.envonly fills in values that are not already set in the process environment. -
env_fileitself cannot use${VAR}interpolation (the env hasn't been loaded yet when that field is read).
Option 2 — GOTEN_DATABASE_URL env override:
If GOTEN_DATABASE_URL is set, it overrides database.url from the file regardless of what (if anything) is in the YAML. Useful when you do not want any database reference in the config file at all:
GOTEN_DATABASE_URL="postgres://user:realpass@prod-host:5432/mydb" goten migrate upEither approach keeps secrets out of version control. Use whichever fits your deployment setup (CI/CD secret stores, .env files in .gitignore, systemd EnvironmentFile=, etc.).
From the repo root:
make exampleThis builds the CLI, starts Postgres, applies migrations, and runs the example server on :8080.
# 1. Start Postgres (from examples/basic/)
cd examples/basic
docker compose up -d
# 2. Build the CLI (from the repo root)
cd ../..
make cli
# 3. Apply migrations (run from examples/basic/ so the CLI picks up goten.config.yaml)
cd examples/basic
../../bin/goten migrate up
# 4. Run the server
go run .You should see listening on :8080. The server exposes:
| Method | Path | Description |
|---|---|---|
GET |
/ |
Lists all routes |
GET |
/health |
Health check |
POST |
/api/auth/sign-up/email |
Register with email + password |
POST |
/api/auth/sign-in/email |
Login with email + password |
POST |
/api/auth/sign-up/username |
Register with username (plugin) |
POST |
/api/auth/sign-in/username |
Login with username (plugin) |
POST |
/api/auth/sign-out |
Revoke current session |
GET |
/api/auth/get-session |
Current session + user |
GET |
/api/auth/list-sessions |
All sessions for current user |
POST |
/api/auth/revoke-session |
Revoke a specific session |
GET |
/api/me |
Example protected route |
| Setting | Default |
|---|---|
| Postgres host:port | localhost:5432 |
| Postgres database | goten |
| Postgres user / pass |
goten / goten
|
| Server URL | http://localhost:8080 |
GOTEN_SECRET |
dev-secret-32-bytes-min-please!! (dev only — override for anything real) |
All of the above can be overridden with environment variables: DATABASE_URL, BASE_URL, GOTEN_SECRET, PORT.
With the server running on :8080, walk through the full flow.
curl -i -X POST http://localhost:8080/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"correct-horse-battery-staple","name":"Alice"}'You get back a JSON body with the new user and session, and a Set-Cookie header containing the session token. The token also appears in the response body as token (starts with g10_). Save it for later requests:
TOKEN="g10_...paste from response..."curl -i -X POST http://localhost:8080/api/auth/sign-in/email \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"correct-horse-battery-staple"}'Same shape — fresh session, fresh cookie, fresh token.
Cookie-based (browser flow):
curl -i http://localhost:8080/api/auth/get-session \
-H "Cookie: goten.session_token=$TOKEN"Bearer token (mobile / API flow):
curl -i http://localhost:8080/api/auth/get-session \
-H "Authorization: Bearer $TOKEN"Both return { "user": {...}, "session": {...} }.
curl -i http://localhost:8080/api/me \
-H "Authorization: Bearer $TOKEN"200 OK when the token is valid, 401 Unauthorized when it's missing, expired, or revoked.
curl -i -X POST http://localhost:8080/api/auth/sign-up/username \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"correct-horse-battery-staple","email":"alice2@example.com"}'
curl -i -X POST http://localhost:8080/api/auth/sign-in/username \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"correct-horse-battery-staple"}'List all sessions for the current user:
curl -i http://localhost:8080/api/auth/list-sessions \
-H "Authorization: Bearer $TOKEN"Revoke a specific session by ID:
curl -i -X POST http://localhost:8080/api/auth/revoke-session \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"session_id":"g10_018f..."}'Revoke all other sessions (keep current):
curl -i -X POST http://localhost:8080/api/auth/revoke-other-sessions \
-H "Authorization: Bearer $TOKEN"curl -i -X POST http://localhost:8080/api/auth/sign-out \
-H "Authorization: Bearer $TOKEN"After this, $TOKEN no longer works on /api/me or /api/auth/get-session.
-
Add more login methods — see Plugins for the Username plugin and instructions on building your own (
Endpoints,SchemaProvider,MigrationProvider, hook providers). -
Manage migrations —
goten migrate status / up / down / generate <name>. See the CLI section in the README. -
Harden for production — set
TrustedOriginsto your real domain(s) so non-bearer requests are origin-checked. See CSRF Protection. - Understand the module layout — Goten is split into core, adapters, plugins, and CLI as separate Go modules. See Architecture.
If something doesn't work, double-check the two gotchas from step 3: the Secret length and the BaseURL matching the URL your client actually reaches.