Techadamia Backend - Development Guide
Guide for local development, building, running, regenerating SQL code, and testing.
Table of Contents
- Prerequisites
- Local Setup
- Building
- Running
- Regenerating SQL Code (sqlc)
- Email flows without an SMTP server
- Testing
- Migrations
- Troubleshooting
Prerequisites
Required
- Go 1.26 - the module targets
go 1.26.1(seego.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 usesredis: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_HOSTthe 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 aTaskfile.ymlwithtask startandtask testhelpers. 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_devAPP_PORT- e.g.8080MEDIA_STORAGE_DIR- e.g../uploads(created automatically if missing; must be writable)SESSION_COOKIE_SECURE-falsefor local HTTP development (must betruewhenAPP_ENV=production)CORS_ALLOWED_ORIGINS- e.g.http://localhost:3000APP_PUBLIC_URL- e.g.http://localhost:3000; the base for password-reset and email-verification links. Falls back to the firstCORS_ALLOWED_ORIGINSentry 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:
- Loads configuration from the environment (
.envauto-loaded if present) and fails fast on invalid config. - Connects to PostgreSQL using a tuned
pgxpool. - Applies any pending migrations automatically (see Migrations).
- 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:
-
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 firstCORS_ALLOWED_ORIGINSentry), so point that at your frontend. -
Readers skip verification.
POST /registerwithrole=readeractivates the account immediately, so you can register and log in in one step. The response'sverification_requiredfield 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
TestMainfunctions readTEST_DATABASE_URL. If it is unset, the suites print a notice and exit 0 (skipped) - sogo 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 is009_…. - Applied versions are tracked in the
schema_migrationstable; already-applied files are skipped. - Application is guarded by a
pg_advisory_lockheld 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.sqlis the worked example:ALTER TYPE … ADD VALUEcannot 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
- Confirm PostgreSQL is running and reachable.
- Verify
DATABASE_URLis 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