Skip to content

Quick Start

dnahilman edited this page Jun 16, 2026 · 9 revisions

Quick Start

This guide walks you from zero to a working Goten-backed HTTP server in a brand-new Go project. By the end you will have:

  • A configured goten.config.yaml
  • Generated GORM models for the core auth schema (and any plugins)
  • A main.go that applies the schema with AutoMigrate and exposes sign-up / sign-in / protected routes
  • A verified end-to-end flow using curl

If you just want a runnable reference instead of building from scratch, see examples/basic/ in the repo. The footer of this page also links back there as a safety net.

Contents

  1. Prerequisites
  2. Initialize the project
  3. Install the CLI
  4. Configure: goten.config.yaml
  5. Generate the models
  6. Write main.go
  7. Run and test with curl
  8. Adding a plugin later
  9. Next steps

1. Prerequisites

  • Go 1.25+go version. (Goten's modules declare go 1.25.)
  • A PostgreSQL database — any reachable Postgres works (local, managed, RDS, Neon, Supabase, …).

No Postgres locally? One-shot Docker container:

docker run -d --name pg \
  -e POSTGRES_USER=goten -e POSTGRES_PASSWORD=goten -e POSTGRES_DB=goten \
  -p 5432:5432 postgres:16-alpine

The DSN for this is postgres://goten:goten@localhost:5432/goten?sslmode=disable.


2. Initialize the project

mkdir myapp && cd myapp
go mod init github.com/yourname/myapp

# Core + GORM adapter
go get github.com/dnahilman/goten
go get github.com/dnahilman/goten/adapters/gorm

# Optional: username plugin (login by username instead of/alongside email)
go get github.com/dnahilman/goten/plugins/username

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


3. Install the CLI

go install github.com/dnahilman/goten/cmd/goten@latest

After this, goten should be on your PATH. Verify with goten --help.

The CLI is generate-only (like better-auth's generate): it emits ORM model definitions from the core schema plus the active plugins' schema. You apply them with your ORM — for GORM, db.AutoMigrate(authmodels.AllModels()...). There is no separate migrate command; your ORM owns DDL.


4. Configure: goten.config.yaml

Create goten.config.yaml at the project root:

# yaml-language-server: $schema=https://raw.githubusercontent.com/dnahilman/goten/main/goten.config.schema.json
plugins:
  - username        # delete this line if you skipped the optional plugin

generate:
  output_dir: ./internal/auth   # where the generated file lands
  package: authmodels           # Go package name for the generated file
  orm: gorm                     # currently only "gorm"

Editor autocomplete: the magic comment on line 1 makes VS Code (with the Red Hat YAML extension), JetBrains, and other LSP-aware editors give field autocomplete, inline docs on hover, and red squiggles on typos.

The defaults (when omitted) are output_dir: ./internal/auth, package: authmodels, orm: gorm.


5. Generate the models

goten generate

This writes a single file — internal/auth/auth_models.go by default — containing the GORM model structs for the core schema plus every plugin listed in §4:

.
├── goten.config.yaml
└── internal/
    └── auth/
        └── auth_models.go     # User, Session, Account, Verification (+ plugin fields)

The file is marked // Code generated by goten generate. DO NOT EDIT. and exposes an AllModels() helper so you can pass everything to AutoMigrate in one call. Re-run goten generate whenever you change the plugin list or bump goten; pass -y to overwrite without the prompt.

Versioned migrations instead of AutoMigrate? Feed the generated structs to a schema tool like Atlas (atlas-provider-gorm) to produce versioned SQL. See examples/ and the README for the pattern.


6. Write main.go

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"

    authmodels "github.com/yourname/myapp/internal/auth" // the generated package
)

func main() {
    db, err := gorm.Open(
        postgres.Open("postgres://goten:goten@localhost:5432/goten?sslmode=disable"),
        &gorm.Config{},
    )
    if err != nil {
        log.Fatal(err)
    }

    // Apply goten's schema. AutoMigrate is additive (creates tables, adds
    // columns/indexes); it never drops or rewrites. Run it on boot or as a
    // one-off — for production prefer a versioned migration tool.
    if err := db.AutoMigrate(authmodels.AllModels()...); err != nil {
        log.Fatal(err)
    }

    auth, err := goten.New(goten.Config{
        // BaseURL MUST match the URL clients use to reach the server.
        // Mismatches break cookie scoping and the CSRF origin check.
        BaseURL: "http://localhost:8080",

        // Secret MUST be >= 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)
    }

    http.Handle("/api/auth/", auth.Handler())

    http.Handle("/api/me", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, ok := goten.UserFromContext(r.Context())
        if !ok {
            // RequireAuth already short-circuits unauthenticated requests,
            // so this branch is mostly a guard for future refactors.
            http.Error(w, "no user", http.StatusInternalServerError)
            return
        }
        _ = 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 clients reach. A mismatch breaks cookie scoping and the CSRF origin check.

Using Gin instead of net/http? Same setup — see Goten with Gin for the gin.WrapH mounting pattern and a Gin-idiomatic middleware adapter.

Run it:

go run .

7. Run and test with curl

With the server on :8080, walk through the full flow. Goten issues an HttpOnly cookie (goten_session) and also returns the token in the JSON body at session.token (prefixed g10_) for non-browser clients.

Sign up

curl -i -c cookies.txt -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"}'

The response body is {"user":{...},"session":{...}}. Grab the bearer token from session.token if you want to test with Authorization headers instead of cookies:

TOKEN="g10_...paste session.token from the response..."

Sign in

curl -i -c cookies.txt -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"}'

Get current session

Cookie:

curl -i -b cookies.txt http://localhost:8080/api/auth/get-session

Bearer:

curl -i http://localhost:8080/api/auth/get-session \
  -H "Authorization: Bearer $TOKEN"

Protected route

curl -i -b cookies.txt http://localhost:8080/api/me
# or: -H "Authorization: Bearer $TOKEN"

200 OK when authenticated; 401 Unauthorized otherwise.

Sign out

curl -i -b cookies.txt -X POST http://localhost:8080/api/auth/sign-out

Username plugin (if enabled in §4)

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
curl -i -b cookies.txt http://localhost:8080/api/auth/list-sessions

# Revoke one (note: camelCase "sessionId")
curl -i -b cookies.txt -X POST http://localhost:8080/api/auth/revoke-session \
  -H "Content-Type: application/json" \
  -d '{"sessionId":"g10_018f..."}'

# Revoke all other sessions
curl -i -b cookies.txt -X POST http://localhost:8080/api/auth/revoke-other-sessions

8. Adding a plugin later

Adding a plugin is a 4-step affair:

# 1. Pull the plugin
go get github.com/dnahilman/goten/plugins/<plugin>

# 2. Add it to goten.config.yaml under plugins:
#      plugins:
#        - <plugin>

# 3. Re-generate the models (overwrites the generated file)
goten generate -y

# 4. Apply the new schema — AutoMigrate picks up the new tables/columns on boot

Then register the plugin in your goten.New(...) call:

import usernameplugin "github.com/dnahilman/goten/plugins/username"

auth, _ := goten.New(goten.Config{
    // ...
    Plugins: []goten.Plugin{
        usernameplugin.New(usernameplugin.Options{}),
    },
})

Restart the app — the new endpoints (e.g. /api/auth/sign-up/username) are now live.

The OAuth plugin (plugins/oauth, Sign in with Google) follows the same flow but needs a provider configured and TrustedOrigins set. See examples/oauth-google/ and the README OAuth section.


9. Next steps

  • More plugins / build your own — see Plugins in the README for the interfaces (Endpoints, SchemaProvider, hook providers).
  • Front-end — wire a typed client with the React + TypeScript example.
  • Mount under Gin / Echo / Chi — see Goten with Gin; the pattern transfers to any framework that accepts http.Handler.
  • Harden for production — set TrustedOrigins to your real domain(s) so non-bearer requests are origin-checked. See CSRF Protection. Behind a reverse proxy, configure the client-IP header per SECURITY.md.
  • Understand the module layout — Goten is split into core, adapters, plugins, and CLI as separate Go modules. See Architecture.

Still stuck or want a complete reference? examples/basic/ is a runnable end-to-end project (Postgres via Docker Compose, full Makefile) — clone the repo and you have a working app to compare against your own.