Files
Snip/backend
2026-07-26 14:56:21 +07:00
..
2026-06-15 21:25:57 +07:00
2026-07-26 14:56:21 +07:00
2026-06-15 21:25:57 +07:00
2026-06-15 21:25:57 +07:00
2026-06-15 21:25:57 +07:00

snip backend

Go service for the snip URL shortener. Hexagonal (ports & adapters) with constructor-based dependency injection, Postgres for source of truth, Redis as the redirect hot-path cache.

One binary, three commands

cmd/snip builds a single binary with three subcommands so one Docker image can back three docker-compose services:

Command Default port Serves
api 8080 JSON API under /api/v1/* (links CRUD, PIN, OAuth/OIDC login)
frontend 8081 The built SPA — owns /, /login, /dashboard + assets
redirect 8082 GET /{code} → 302, plus the fast self-contained enter-PIN page
PORT=8080 ./snip api
PORT=8081 ./snip frontend
PORT=8082 ./snip redirect

In production a gateway routes by path (/api/*→api, app routes→frontend, the rest→redirect). See the repo-root Caddyfile + docker-compose.yml.

Architecture (hexagon)

cmd/snip                    entrypoint: parses subcommand, graceful shutdown
internal/
  domain/                   entities + sentinel errors (no deps)
  port/                     interfaces the core depends on (the hexagon edges)
  service/                  application core — depends only on ports
    link_service.go         create/list/update/delete/pin
    redirect_service.go     cache-first resolve + pin verify (hot path)
    clicks.go               batched async click recorder
    auth_service.go         OAuth/OIDC login + sessions
    shortcode.go            base62 codes, validation, reserved-path guard
  adapter/                  implementations of the ports
    memory/                 in-memory (tests + STORE=memory)
    postgres/               pgxpool repos + advisory-locked migrations
    rediscache/             go-redis resolution cache
    identity/               OIDC provider (covers Google + generic OIDC)
    security/               bcrypt hasher + HMAC session tokens
  httpx/                    inbound HTTP adapters (api / frontend / redirect)
  config/                   env config
  di/                       wires adapters → services per command

The core (service) imports only port and domain; nothing in it knows about Postgres, Redis, HTTP or OAuth. di is the only package that imports concrete adapters.

Performance design

The redirect path is the hot one and is built to avoid Postgres:

  1. GET /{code} reads code:{code} from Redis. On a hit it 302s immediately — no DB.
  2. On a miss it reads Postgres once, then caches the tiny resolution ({longURL, hasPin, pinHash}) with a 24h TTL.
  3. Unknown codes are negatively cached for 60s so scanners can't hammer the DB.
  4. PINs are verified against the cached bcrypt hash — protected links also skip the DB.
  5. Clicks never block the redirect: they're counted in memory and flushed to click_daily (+ links.total_clicks) in batches (CLICK_FLUSH_SECONDS, default 10), collapsing bursts into one write.

Two login methods

Both google and a generic oidc provider implement the same IdentityProvider port (Google is itself an OIDC issuer). Each is optional — absent credentials simply hide that button.

  • GET /api/v1/auth/{provider}/login → 302 to the provider
  • GET /api/v1/auth/{provider}/callback → sets an HttpOnly session cookie, 302s to /dashboard
  • GET /api/v1/auth/me, POST /api/v1/auth/logout, GET /api/v1/auth/providers

Sessions are stateless HMAC-signed tokens (no server store). State is validated with a double-submit cookie.

Reserved paths

Custom aliases can't collide with app-owned paths — api, login, dashboard, assets, healthz, etc. are rejected at creation (service.ReservedCodes).

Configuration (env)

Var Default Notes
STORE postgres or memory (no infra)
DATABASE_URL postgres://snip:snip@localhost:5432/snip?sslmode=disable
REDIS_ADDR / REDIS_PASSWORD / REDIS_DB localhost:6379 / `` / 0
SESSION_SECRET dev default set in prod
PUBLIC_URL http://localhost:8080 builds OAuth callback URLs
POST_LOGIN_REDIRECT /dashboard
SHORT_DOMAIN snip.to display host on the PIN page
FRONTEND_DIST ./web built SPA directory (frontend cmd)
COOKIE_SECURE false set true behind HTTPS
CLICK_FLUSH_SECONDS 10 click batch interval
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET enables Google login
OIDC_ISSUER / OIDC_CLIENT_ID / OIDC_CLIENT_SECRET enables OIDC login

Develop & test

go test ./...                 # unit + HTTP integration tests (in-memory, no infra)
STORE=memory PORT=8080 go run ./cmd/snip api    # run with zero infrastructure

The test suite uses the in-memory adapters, so it needs neither Postgres nor Redis. The real adapters are exercised via docker-compose.