</>Mohit Anand
← Back to projects

scratchpad

Ephemeral text storage with TTL GC — share snippets securely, auto-expire without a trace.

GitHubStack:GolangPostgreSQLPrometheusGrafanaDocker

ScratchPad

CI Status

A server-rendered Go web application for creating and sharing self-destructing text snippets. Sign up, log in, publish a snippet with a title and content, and choose how long it lives (1 day, 1 week, or 1 year) before it disappears.

Features

Tech Stack

Component Technology
Language Go 1.26
HTTP server Standard library net/http (Go 1.22 method + path routing)
Database PostgreSQL 17 (lib/pq)
Sessions alexedwards/scs/v2 with postgresstore
Metrics prometheus/client_golang on a dedicated metrics server
CSRF protection justinas/nosurf
Middleware chaining justinas/alice
Form decoding go-playground/form/v4
Password hashing golang.org/x/crypto/bcrypt
Templating html/template with embedded filesystem
Deployment Podman Compose (Docker Compose compatible)
Observability Prometheus + Grafana (+ pgAdmin for the database)

Getting Started

Prerequisites

podman-compose up -d --build

This builds the app image plus a small migrator image, applies all migrations once the database is healthy, then starts everything else:

Service URL Credentials
ScratchPad (HTTPS) https://localhost:8080 sign up in the app
Grafana http://localhost:3000 admin / password
Prometheus http://localhost:9090 n/a
pgAdmin http://localhost:5050 admin@scratchpad.com / password
PostgreSQL internal only (db:5432 on scratchpad_net) scratchpad / password, db scratchpad

The credentials above are development dummies hardcoded in docker-compose.yml. Change them before exposing this stack anywhere.

The app container mounts ./tls read-only and expects cert.pem/key.pem inside — generate them first (next section) if you don’t have them yet.

Option B — Local development (bare metal)

1. Generate TLS certificates

The server only speaks HTTPS and expects certificates at ./tls/cert.pem and ./tls/key.pem (both are gitignored):

mkdir -p tls
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout tls/key.pem -out tls/cert.pem -days 365 \
  -subj "/CN=localhost"

2. Start PostgreSQL

Either use just the database from the compose stack:

make compose-up          # or: podman-compose up -d db

or point at any existing PostgreSQL instance via DSN.

3. Apply migrations

make migrate-up          # uses ~/go/bin/migrate if not on PATH
# rollback one step: make migrate-down

Raw CLI equivalent:

migrate -path=./migrations \
  -database="postgres://scratchpad:password@localhost/scratchpad?sslmode=disable" up

4. Run the app

make run

Then open https://localhost:8080 (accept the self-signed certificate warning). Metrics are served separately on http://localhost:8081/metrics.

Configuration

Every setting is a CLI flag with an environment-variable fallback:

Setting Flag Environment variable Default
App listen port -port PORT 8080
Metrics listen port -metrics_port METRICS_PORT 8081
PostgreSQL DSN -dsn DSN postgres://scratchpad:password@localhost/scratchpad?sslmode=disable
Max open DB connections -db-max-open-conns 25
Max idle DB connections -db-max-idle-conns 25
Max idle connection time -db-max-idle-time 15m

Note: the app serves over HTTPS only. Sessions and CSRF cookies are marked Secure, so the site won’t function over plain HTTP.

Routes

Method Path Auth Description
GET / Public Home page with the 10 latest pads
GET /pads/view/{id} Public View a single pad
GET /pads/create Required Show the create-snippet form
POST /pads/create Required Create a snippet
GET /user/signup Public Show the signup form
POST /user/signup Public Create an account
GET /user/login Public Show the login form
POST /user/login Public Log in
POST /user/logout Required Log out
GET /ping Public Health check, responds OK
GET /static/ Public Static assets
GET /metrics Internal Prometheus scrape endpoint (separate port)

Metrics

Metrics live on their own listener (default :8081) behind a dedicated Prometheus registry, so scrape traffic never touches the application mux.

Metric Type Labels
scratchpad_http_request_total counter method, endpoint, status_code
scratchpad_http_request_duration_seconds histogram method, endpoint
scratchpad_http_active_requests gauge

The endpoint label is the matched route pattern (e.g. GET /pads/view/{id}), never the raw request path — parameterized URLs can’t blow up label cardinality. Requests that don’t match any route are labeled unmatched. Panics recovered by middleware still produce a recorded 500 series, and the active-request gauge is decremented even on panic paths.

Grafana’s Prometheus datasource is provisioned automatically from grafana/datasources.yaml; the scrape config lives in prometheus/prometheus.yml.

Database & Migrations

Three tracked migrations create the schema: pads, users (unique email constraint), and sessions (token/data/expiry used by scs’s postgresstore).

make migrate-up     # apply all pending
make migrate-down   # roll back the latest
DSN="postgres://user:pass@host/dbname?sslmode=disable" make migrate-up   # custom target

In the container stack, the one-shot migrate service runs automatically before the app starts (depends_on: service_completed_successfully), so podman-compose down && podman-compose up re-applies pending migrations safely.

Tests

make test         # verbose go test ./...
make test-race    # race detector, no caching (same as CI)

Project Structure

cmd/web/              Web application entrypoint: main, routes, handlers,
                      middleware, helpers, templates, context keys
internal/metrics/     Prometheus collectors, registry wiring, /metrics handler
internal/models/      Data access layer for pads and users, sentinel errors,
                      and mocks used by offline tests
internal/validator/   Form validation helpers (embedded in form structs)
internal/assert/      Tiny generic test assertion helpers
migrations/           golang-migrate SQL migrations (up/down pairs)
prometheus/           Scrape config mounted into the Prometheus container
grafana/              Datasource provisioning mounted into Grafana
ui/                   Embedded HTML templates, static CSS, and assets (embed.FS)
Dockerfile            Multi-stage build for the web binary
Containerfile.migrate Builds the pinned golang-migrate CLI image
docker-compose.yml    Full stack: db, migrate, scratchpad, prometheus, grafana, pgadmin
Makefile              Common dev tasks
tls/                  TLS certificates (gitignored)

Makefile Targets

Target Description
make build Build the binary to ./build/web
make fmt Format all Go source files
make run Build and run the server
make test Run the test suite verbosely
make test-race Run the suite with the race detector (-race -count=1)
make migrate-up Apply pending migrations (honors DSN= override)
make migrate-down Roll back the most recent migration
make compose-up Build and start the full container stack
make compose-down Tear down the container stack