Go Application Starter
A Go HTTP starter built with Chi, Turso/libSQL, sqlc, and Gomponents. It includes
server configuration, health checks, metrics, and graceful shutdown, with no
sample domain model or CRUD endpoints.
Included
- Chi routing and middleware
- Local file databases and remote Turso databases
through the libSQL database layer
- sqlc configuration ready for application queries
- Gomponents HTML rendering
- Authentication middleware and a development mock provider for future protected routes
- Request timeouts and size limits, explicit CORS headers, per-client rate
limiting, and HTTPS 1.2 or newer
- Liveness/readiness probes, Prometheus metrics, and optional OTLP tracing
- CI, a multi-stage Docker image, and common Make targets
Requirements
- Go 1.26.5 or newer
sqlc only when regenerating database code
- Docker only when building or running the container image
Quick Start
The example configuration uses a local file:starter-a.db database. The
embedded schema migrations run automatically during application startup.
cp cmd/config.toml config.toml
go run ./cmd
The application listens on http://localhost:8080.
The same startup can be run with the Makefile without copying the config:
make run
Enable debug logging with -v, or select another configuration file with
-c:
go run ./cmd -v -c path/to/config.toml
Configuration
The included cmd/config.toml contains runnable local defaults:
[Server]
Port = "8080"
EnableHTTPS = false
HTTPSPort = "443"
AllowedCorsURLs = ["http://localhost:3000"]
TaskTimeOutInSeconds = 30
ReadTimeoutInSeconds = 15
WriteTimeoutInSeconds = 35
IdleTimeoutInSeconds = 60
RateLimitRequestsPerSecond = 20
RateLimitBurst = 40
ServerDomain = ""
[App]
Environment = "dev"
[Log]
Format = "auto"
Level = "info"
GCPProjectID = ""
[Database]
TursoConnectionString = "file:starter-a.db"
To use Turso, set the connection string through the environment so the token
does not need to be committed:
export TURSO_CONNECTION_STRING='libsql://database-name.turso.io?authToken=token'
go run ./cmd -c cmd/config.toml
Environment variables take precedence over the file. Supported overrides are:
| TOML setting |
Environment variable |
App.Environment |
ENVIRONMENT |
Server.Port |
SERVER_PORT |
Server.EnableHTTPS |
ENABLE_HTTPS |
Server.HTTPSPort |
HTTPS_SERVER_PORT |
Server.AllowedCorsURLs |
ALLOWED_CORS_URLS |
Server.TaskTimeOutInSeconds |
TASK_TIMEOUT_IN_SECONDS |
Server.ReadTimeoutInSeconds |
READ_TIMEOUT_IN_SECONDS |
Server.WriteTimeoutInSeconds |
WRITE_TIMEOUT_IN_SECONDS |
Server.IdleTimeoutInSeconds |
IDLE_TIMEOUT_IN_SECONDS |
Server.RateLimitRequestsPerSecond |
RATE_LIMIT_REQUESTS_PER_SECOND |
Server.RateLimitBurst |
RATE_LIMIT_BURST |
Server.ServerDomain |
SERVER_DOMAIN |
Database.TursoConnectionString |
TURSO_CONNECTION_STRING |
Log.Format |
LOG_FORMAT |
Log.Level |
LOG_LEVEL |
Log.GCPProjectID |
GOOGLE_CLOUD_PROJECT |
Local SQLite uses one pooled connection, so concurrent requests queue rather than compete for the database write lock. Each connection also waits up to five seconds for locks held by another process. Override this wait in milliseconds with file:starter-a.db?_busy_timeout=10000. Existing SQLite URL options are preserved. Remote Turso retains a pool of up to 25 connections.
Keep transactions short. While holding a transaction, execute its queries through that transaction rather than the database pool; the single local connection remains occupied until commit or rollback. Sustained lock contention can still return an error after the wait expires.
When HTTPS is enabled, ServerDomain is required and must resolve to the
server. The HTTP and HTTPS listeners are both shut down gracefully, and the
ACME certificate cache is written under certs/.
Authentication and Routes
The home page and operational endpoints are public. No application API routes
are included. Authentication middleware remains in internal/auth/ for future
protected routes; its mock provider is for development only. Use a production
implementation of auth.Servicer when adding authentication, and add CSRF
protection for cookie-authenticated write routes.
| Method |
Path |
Authentication |
Description |
GET |
/ |
No |
Render the home page |
GET |
/healthz |
No |
Process liveness |
GET |
/readyz |
No |
Database readiness |
GET |
/metrics |
No |
Prometheus-compatible metrics |
Logging
Logs go to stderr through log/slog. Log.Format chooses the encoding:
| Format |
Behavior |
auto (default) |
Text when started with go run or when stderr is a terminal; JSON otherwise |
text |
Human-readable key=value lines |
json |
One JSON object per line using Cloud Logging field names (severity, message, time) |
The go run check looks at whether the executable lives in a go-build cache directory, so a deployed binary in a container gets JSON without any flags. --verbose forces debug level regardless of Log.Level. When Log.GCPProjectID (or GOOGLE_CLOUD_PROJECT) is set, request logs include the logging.googleapis.com/trace fields so Cloud Run log lines link to Cloud Trace.
Operations and Observability
User-facing routes use a per-client in-memory token bucket. Health, readiness,
and metrics remain available during traffic spikes. Behind a reverse proxy,
rate-limit at the edge or change the client-key logic to trust only that
proxy's forwarding headers.
Prometheus can scrape /metrics. Nonstandard HTTP methods share the OTHER
label, keeping request counter and duration metric categories bounded. Restrict it at the network or reverse-proxy
layer if operational data should not be public.
OTLP/HTTP trace export is disabled by default. Enable it with standard
OpenTelemetry environment variables:
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
The application samples 10% of new root traces while preserving parent
sampling decisions. Request logs include trace and span IDs when available.
Development
make test # go test ./...
make vet # go vet ./...
make build # bin/starter-a
make generate # sqlc generate
make docker-build
Add numbered .sql migrations to internal/database/schema/ and sqlc queries
to internal/database/queries/. Then run make generate and commit the generated
internal/database/repo/ files. Until queries are added, make generate is a
no-op. A fresh database contains only migration bookkeeping; existing database
tables are left intact.
cmd/main.go loads configuration, initializes shared resources, and runs the web
server. Add domain services and repository wiring as your application needs them.
internal/app/ assembles shared dependencies and owns their lifecycle.
internal/server/ builds the HTTP middleware chain and owns server lifecycle.
internal/logging/ builds the logger and selects text or JSON output.
internal/handlers/http/ adapts HTTP requests and responses.
internal/routes/ maps endpoints and applies route-level middleware.
internal/database/ owns connections and embedded migrations, with directories
ready for application schema and sqlc queries.
internal/auth/ contains the auth contract, middleware, and mock provider.
internal/ui/ contains Gomponents views.
Docker
docker build -t starter-a .
docker run --rm -p 8080:8080 -v starter-a-data:/data starter-a
The image runs as an unprivileged user and stores the default local database in
the /data volume.
License
This project is available under the MIT License.