Techadamia Backend Documentation

Techadamia Backend - Development Guide

Guide for local development, building, running, regenerating SQL code, and testing.

Table of Contents

  1. Prerequisites
  2. Local Setup
  3. Building
  4. Running
  5. Regenerating SQL Code (sqlc)
  6. Email flows without an SMTP server
  7. Testing
  8. Migrations
  9. Troubleshooting

Prerequisites

Required

  • Go 1.26 - the module targets go 1.26.1 (see go.mod). Download from golang.org.
  • PostgreSQL - any reasonably recent server. The Docker stack uses postgres:16.

Optional

  • Redis - only needed if you want to exercise shared/distributed rate limiting locally (set RATE_LIMIT_REDIS_URL). The Docker stack uses redis:7-alpine. Without it, rate limiting falls back to in-memory storage.
  • An SMTP server - only needed if you want real password-reset and email-verification mail. Without SMTP_HOST the app still runs and the flows still work — see Email flows without an SMTP server.
  • sqlc - required only if you modify SQL queries and need to regenerate the typed Go code. Install with go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest.
  • go-task (task) - optional task runner. The repo ships a Taskfile.yml with task start and task test helpers. Install from taskfile.dev.
  • Docker / Docker Compose - for the containerized stack (see DEPLOYMENT.md).

Local Setup

# Clone and enter the repo
git clone https://github.com/S3-R4/Techadamia.git
cd techadamia-backend

# Download dependencies
go mod download

# Copy the environment template and fill it in
cp .env.example .env

Edit .env and set at least the required variables:

  • DATABASE_URL - e.g. postgresql://postgres:password@localhost:5432/techadamia_dev
  • APP_PORT - e.g. 8080
  • MEDIA_STORAGE_DIR - e.g. ./uploads (created automatically if missing; must be writable)
  • SESSION_COOKIE_SECURE - false for local HTTP development (must be true when APP_ENV=production)
  • CORS_ALLOWED_ORIGINS - e.g. http://localhost:3000
  • APP_PUBLIC_URL - e.g. http://localhost:3000; the base for password-reset and email-verification links. Falls back to the first CORS_ALLOWED_ORIGINS entry when unset.

.env is loaded automatically on startup via godotenv; a missing .env only logs a warning. The full set of configuration options (rate limits, security headers, DB pool tuning, SMTP, BOOTSTRAP_TOKEN, etc.) is documented inline in .env.example.

Create the database that DATABASE_URL points at if it does not already exist:

createdb techadamia_dev

Migrations run automatically on startup, so there is nothing else to provision.


Building

# Build the server binary
go build ./cmd

This produces a cmd binary (named after the package directory). You can also name the output explicitly:

go build -o bin/server ./cmd

Running

# Run directly from source
go run ./cmd

On startup the server:

  1. Loads configuration from the environment (.env auto-loaded if present) and fails fast on invalid config.
  2. Connects to PostgreSQL using a tuned pgx pool.
  3. Applies any pending migrations automatically (see Migrations).
  4. Starts the HTTP server on APP_PORT.

Warning

There is no migrate subcommand — don't run go run ./cmd migrate. Migrations are applied automatically on every normal startup.

The only subcommand is healthcheck:

./server healthcheck   # GETs http://127.0.0.1:$APP_PORT/health, exits 0 on 200 else 1

This is what the Docker container's healthcheck uses (the distroless runtime image has no shell or curl).

The API is available at http://localhost:8080 (or whatever APP_PORT you set).

After first start, create the initial admin once via the bootstrap endpoint - see the "Create the first admin" section of README.md and the admin bootstrap notes in DEPLOYMENT.md.


Regenerating SQL Code (sqlc)

Queries live in internal/sql/query.sql and the schema sqlc reads is internal/sql/schema.sql (configured in sqlc.yaml). After editing a query, regenerate the typed Go code:

sqlc generate

This rewrites the generated files in internal/sql/ (package sql, pgx/v5 driver). Commit the regenerated code alongside your query changes.


Email flows without an SMTP server

Password reset and email verification work locally with no mail server. Leaving SMTP_HOST unset puts the mailer in disabled mode, and the app logs at startup:

WARN SMTP not configured; emails will be logged and readers activate without verification

Two things change in that mode:

  1. Links are logged instead of sent. Reset and verification links go to the server log, so you can copy them straight out of your terminal:

    INFO email verification requested (mailer disabled) username=alice
         verification_link=http://localhost:3000/verify-email?token=6f1c…
    

    The link's host comes from APP_PUBLIC_URL (or the first CORS_ALLOWED_ORIGINS entry), so point that at your frontend.

  2. Readers skip verification. POST /register with role=reader activates the account immediately, so you can register and log in in one step. The response's verification_required field reports which path was taken.

Warning

This is a development convenience only. In production, always configure SMTP — otherwise anyone can create an active account against an email address they do not control. DEPLOYMENT.md covers the SMTP_* variables.

To test against a real mail path without sending anything, point the SMTP_* variables at a local catch-all SMTP server (MailHog, Mailpit, or python -m aiosmtpd -n -l localhost:1025).

Author registration is unaffected: authors always go through admin approval, whether or not SMTP is configured.


Testing

Integration tests live in internal/api (HTTP/handler tests, ~66 cases across 10 files) and internal/service (service-layer tests, ~4 cases). Both suites are integration tests that talk to a real PostgreSQL database.

The internal/api suite is split by feature area — auth_test.go, accountflows_test.go (password reset, verification), posts_test.go, feed_test.go (recent, search), profile_test.go, social_test.go (comments, likes, follows), messages_test.go, media_test.go, admin_test.go — with the shared harness in main_test.go. Put a new handler test next to its feature rather than in main_test.go.

Test database

Set TEST_DATABASE_URL to a disposable Postgres database before running the tests:

  • Both TestMain functions read TEST_DATABASE_URL. If it is unset, the suites print a notice and exit 0 (skipped) - so go test ./... is safe with no database configured.
  • When set, each suite runs the embedded migrations against that database on startup and the tests TRUNCATE/reset tables between cases.

Caution

Point TEST_DATABASE_URL at a throwaway database. The suites TRUNCATE and reset tables between cases — never aim it at real data.

Running the tests

Run the suites serialized with -p 1:

TEST_DATABASE_URL=postgres://postgres:password@localhost:5432/techadamia_test \
  go test -p 1 ./...

Important

Pass -p 1. Both DB-backed packages share one database; the migration advisory lock makes concurrent migration safe, but data isolation needs the suites to run one package at a time. Without -p 1 you'll get flaky failures from interleaved truncates.

The provided task wraps the same command (and warns if TEST_DATABASE_URL is unset):

task test    # runs: go test -p 1 -count=1 ./...

Standing up a throwaway Postgres

You don't need a permanent server. Either spin up a temporary local cluster:

# Create a temp cluster on a spare port, start it, create the test DB
TMPPG=$(mktemp -d)
initdb -D "$TMPPG"
pg_ctl -D "$TMPPG" -o "-p 5433" -l "$TMPPG/log" start
createdb -p 5433 techadamia_test

export TEST_DATABASE_URL=postgres://$(whoami)@localhost:5433/techadamia_test
go test -p 1 ./...

# Tear it down when done
pg_ctl -D "$TMPPG" stop && rm -rf "$TMPPG"

...or use Docker:

docker run -d --name tdtest -e POSTGRES_PASSWORD=password \
  -p 5433:5432 postgres:16

export TEST_DATABASE_URL=postgres://postgres:password@localhost:5433/postgres
go test -p 1 ./...

docker rm -f tdtest

Migrations

Migrations are embedded SQL files under internal/migrate/migrations/, applied automatically on startup by internal/migrate (called from cmd/main.go):

  • Files are applied in filename order (lexicographic), so prefix new files with a zero-padded number. The tree currently runs through 008_post_search.sql, so the next one is 009_….
  • Applied versions are tracked in the schema_migrations table; already-applied files are skipped.
  • Application is guarded by a pg_advisory_lock held on a single connection, so multiple replicas booting at once is safe - only one applies migrations at a time.
  • Each file runs inside a transaction unless it contains the marker -- +no-transaction, in which case it runs outside a transaction (for statements that cannot run in one). 004_add_reader_role.sql is the worked example: ALTER TYPE … ADD VALUE cannot be used inside the transaction that adds it.

To add a migration, drop a new numbered .sql file into internal/migrate/migrations/ and start the server (or run the tests) to apply it.

Important

Migrations are the schema's source of truth at runtime, but sqlc reads internal/sql/schema.sql. When a migration changes the schema, mirror the change into schema.sql and re-run sqlc generate, or the generated models will drift from the live database.


Troubleshooting

Server won't start - invalid configuration

The server validates config on startup and exits non-zero with a descriptive error (e.g. a missing required variable, SESSION_COOKIE_SECURE not true under APP_ENV=production, an unwritable MEDIA_STORAGE_DIR, or an invalid RATE_LIMIT_REDIS_URL/TRUSTED_PROXIES). Check the logged error and your .env.

Database connection issues

  1. Confirm PostgreSQL is running and reachable.
  2. Verify DATABASE_URL is correct: psql "$DATABASE_URL" -c "SELECT 1".

Tests are being skipped

That is expected when TEST_DATABASE_URL is unset - the suites exit 0. Set it to a disposable Postgres and re-run with go test -p 1 ./....

Flaky test failures

Make sure you pass -p 1. The two DB-backed suites share one database and must not run in parallel.

No verification or reset email arrives

Expected when SMTP_HOST is unset — the link is written to the server log instead. See Email flows without an SMTP server. If SMTP is configured, the send happens in a background goroutine, so look for a failed to send verification email line in the log rather than an error in the HTTP response: these endpoints deliberately return the same success response either way.

The /events SSE stream disconnects every few seconds

Usually a proxy buffering or timing out the response. The handler already sends X-Accel-Buffering: no and a 25-second heartbeat; if you have your own proxy in front, make sure it does not buffer text/event-stream or impose a shorter read timeout. EventSource reconnects on its own, so this shows up as repeated connected events rather than a hard failure.


Last Updated: 2026-08-17

Generated from DEVELOPMENT.md on 2026-08-17 23:37 CEST. Edit the Markdown source and re-run task docs to update.