Files
Snip/backend/README.md
T
2026-06-15 21:25:57 +07:00

115 lines
5.0 KiB
Markdown

# 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 |
```bash
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
```bash
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.