-
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 in a brand-new Go project. By the end you will have:
- A configured
goten.config.yaml+.env - Core auth schema migrated to a real PostgreSQL database
- A
main.goexposing 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.
- Prerequisites
- Initialize the project
- Install the CLI
- Configure:
goten.config.yaml+.env - Scaffold migrations
- Apply migrations
- Write
main.go - Run and test with
curl - Adding a plugin later
- Next steps
-
Go 1.23+ —
go version. - 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-alpineThe DSN for this is
postgres://goten:goten@localhost:5432/goten?sslmode=disable.
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/usernameEach module is independently versioned — go get only what you actually use.
go install github.com/dnahilman/goten/cmd/goten@latestAfter this, goten should be on your PATH. Verify with goten --help.
Create goten.config.yaml at the project root:
# yaml-language-server: $schema=https://raw.githubusercontent.com/dnahilman/goten/main/goten.config.schema.json
env_file: .env
database:
url: ${DATABASE_URL}
migrations:
plugins:
- username # delete this line if you skipped the optional pluginEditor 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.
Create .env next to it:
DATABASE_URL=postgres://goten:goten@localhost:5432/goten?sslmode=disableAnd add .env to .gitignore:
echo ".env" >> .gitignoreThat's it for credentials. The CLI auto-loads .env from the project directory before resolving ${DATABASE_URL} in the YAML, so no export is needed and no secret lives in version control.
goten initThis copies the core auth migrations (and any plugins listed in §4) into your project:
.
├── goten.config.yaml
├── .env
├── migrations/ # core auth: users, sessions, accounts
│ ├── 20260520120000_initial.up.sql
│ └── 20260520120000_initial.down.sql
└── plugins/
└── username/
└── migrations/ # only if you listed `- username`
├── 20260520130000_add_username.up.sql
└── 20260520130000_add_username.down.sql
goten init is idempotent: re-running it later (e.g. after adding another plugin) only writes new files. Existing files with identical content are skipped silently; files with different content require --force to overwrite.
goten migrate upOutput:
Applying 2 migration(s)...
→ 20260520120000_initial [core] ... OK
→ 20260520130000_add_username [username] ... OK
✓ Done
You can re-check anytime with goten migrate status.
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 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:
-
Secretmust be ≥ 32 bytes. Goten refuses to start with a shorter key. Use a real random value in production. -
BaseURLmust 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 thegin.WrapHmounting pattern and a Gin-idiomatic middleware adapter.
Run it:
go run .With the server 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"}'Response includes a session token (in Set-Cookie and in the JSON body as token, prefixed g10_). Save it:
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"}'Cookie:
curl -i http://localhost:8080/api/auth/get-session \
-H "Cookie: goten.session_token=$TOKEN"Bearer:
curl -i http://localhost:8080/api/auth/get-session \
-H "Authorization: Bearer $TOKEN"curl -i http://localhost:8080/api/me \
-H "Authorization: Bearer $TOKEN"200 OK when the token is valid; 401 Unauthorized otherwise.
curl -i -X POST http://localhost:8080/api/auth/sign-out \
-H "Authorization: Bearer $TOKEN"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
curl -i http://localhost:8080/api/auth/list-sessions \
-H "Authorization: Bearer $TOKEN"
# Revoke one
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
curl -i -X POST http://localhost:8080/api/auth/revoke-other-sessions \
-H "Authorization: Bearer $TOKEN"The same goten init command makes adding a plugin a 4-step affair:
# 1. Pull the plugin
go get github.com/dnahilman/goten/plugins/<plugin>
# 2. Add it to goten.config.yaml under migrations.plugins:
# plugins:
# - <plugin>
# 3. Re-scaffold (existing files are skipped; new plugin files are written)
goten init
# 4. Apply the new migrations
goten migrate upThen 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.
-
More plugins / build your own — see Plugins in the README for the interfaces (
Endpoints,SchemaProvider,MigrationProvider, hook providers). -
Mount under Gin / Echo / Chi — see Goten with Gin; the pattern transfers to any framework that accepts
http.Handler. -
Manage migrations —
goten migrate status / up / down / generate <name>. See CLI. -
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.
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,make example, and you have a working app to compare against your own.