mitbringsl/backend/cmd/server/main.go
Tronax 2899eb205b
Backend Phase A: foundation, migrations, Docker setup
- Go backend skeleton: config (caarlos0/env), slog JSON logging,
  pgxpool store, HTTP server with graceful shutdown.
- httpapi: render helpers, Problem errors, middleware chain
  (requestID / logging / recover / CORS), /healthz and /readyz.
- Migrations: full initial schema (users, sessions, lists,
  list_members, items, op_log SOURCE OF TRUTH, item_names) +
  golang-migrate runner binary using source/iofs (embedded).
- Docker: multi-stage Dockerfile (Go 1.26 -> distroless nonroot),
  builds both server and migrate binaries.
- deploy: docker-compose (caddy + backend + migrate + postgres:16),
  Caddyfile (auto-HTTPS), .env.example, pg extensions init script.
- AGENTS.md: project context + roadmap for AI agents.

Verified: image builds, both binaries run in container (smoke test).
2026-08-05 15:14:37 +02:00

87 lines
2 KiB
Go

// Package main is the entrypoint for the mitbringsl backend HTTP server.
package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/mitbringsl/backend/internal/config"
"github.com/mitbringsl/backend/internal/httpapi"
"github.com/mitbringsl/backend/internal/logging"
"github.com/mitbringsl/backend/internal/store"
)
func main() {
if err := run(); err != nil {
slog.Error("fatal error", "error", err)
os.Exit(1)
}
}
func run() error {
cfg, err := config.Load()
if err != nil {
return err
}
logging.Init(cfg.LogLevel)
slog.Info("starting mitbringsl backend",
"env", cfg.AppEnv,
"addr", cfg.HTTPAddr,
"google_oidc_enabled", cfg.GoogleOIDC.Enabled,
"generic_oidc_enabled", cfg.GenericOIDC.Enabled,
)
rootCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
pool, err := store.New(rootCtx, cfg)
if err != nil {
return err
}
defer pool.Close()
slog.Info("database connected")
api := httpapi.NewAPI(cfg, pool)
srv := &http.Server{
Addr: cfg.HTTPAddr,
Handler: api.Handler(),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
errCh := make(chan error, 1)
go func() {
slog.Info("http server listening", "addr", cfg.HTTPAddr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
}()
select {
case <-rootCtx.Done():
slog.Info("shutdown signal received")
case err := <-errCh:
return err
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("graceful shutdown failed", "error", err)
}
slog.Info("bye")
return nil
}
// keep pgxpool import referenced even when API holds the pool directly (future-proof).
var _ = (*pgxpool.Pool)(nil)