Skip to content

Quick Start

dnahilman edited this page May 26, 2026 · 9 revisions

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.

Contents

  1. Prerequisites
  2. Install the library
  3. Minimal main.go
  4. Run the bundled example
  5. Try it end-to-end with curl
  6. Next steps

1. Prerequisites

  • Go 1.23+go version
  • Docker + Docker Compose — used to run the Postgres dev database. Any local Postgres works too; just point DATABASE_URL at it.
  • make (optional but recommended) — the repo ships a Makefile with all-in-one targets like make example.

You do not need to install Postgres on the host; the example uses a containerised Postgres 16.


2. Install the library

For a new project, pull in core and the GORM adapter:

go get github.com/dnahilman/goten
go get github.com/dnahilman/goten/adapters/gorm

If you want username-based login as well, also pull the username plugin:

go get github.com/dnahilman/goten/plugins/username

Each module is independently versioned — go get only what you actually use.


3. Minimal main.go

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:

  • Secret must be ≥ 32 bytes. Goten refuses to start with a shorter key. Use a real random value in production.
  • BaseURL must 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.


4. Run the bundled example

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 CLI config file: goten.config.yaml

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: ./migrations

Place 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 up

Keeping credentials out of the config file

Do 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: ./migrations
export DATABASE_URL="postgres://user:realpass@prod-host:5432/mydb"
goten migrate up        # clean — no inline secret in the command

Any 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.

Using a .env file (no export needed)

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/mydb
goten migrate up        # .env auto-loaded, ${DATABASE_URL} in YAML resolves

For 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: postgres
goten migrate up        # picks up env_file from the config

B) Pass it on the command line (Docker-style, useful for one-off overrides):

goten --env-file ./config/.env.staging migrate up

Precedence when both are set: --env-file flag > env_file in YAML > default .env in CWD.

Notes:

  • A missing default .env is silently ignored; a missing explicit env_file (either flag or YAML) is a fatal error — explicit means you meant it.
  • Real environment variables always win — .env only fills in values that are not already set in the process environment.
  • env_file itself 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 up

Either approach keeps secrets out of version control. Use whichever fits your deployment setup (CI/CD secret stores, .env files in .gitignore, systemd EnvironmentFile=, etc.).

Option A — all in one command

From the repo root:

make example

This builds the CLI, starts Postgres, applies migrations, and runs the example server on :8080.

Option B — step by step

# 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

Default credentials and ports

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.


5. Try it end-to-end with curl

With the server running on :8080, walk through the full flow.

Sign up with email

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..."

Sign in with email

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.

Get the current session

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": {...} }.

Hit a protected route

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.

Username plugin

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"}'

Session management

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"

Sign out

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.


6. Next steps

  • Add more login methods — see Plugins for the Username plugin and instructions on building your own (Endpoints, SchemaProvider, MigrationProvider, hook providers).
  • Manage migrationsgoten migrate status / up / down / generate <name>. See the CLI section in the README.
  • Harden for production — set TrustedOrigins to 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.

Clone this wiki locally