# Content service in Go A Go implementation of the Backend/DevOps interview service, backed by PostgreSQL. It serves users, posts, comments, and tags. The original Django application remains in the repository as a reference for behavior tests and performance comparisons. See [assignment notes](NOTES.md) for the decisions and [measured comparison](reports/comparison.md) for results and limitations. The original brief and Django setup instructions are in [ASSIGNMENT.md](ASSIGNMENT.md). ## Run locally Requirements: - Go 1.26 or later. Tested with Go 1.26.4. - A running PostgreSQL server and its command-line tools, including `psql`. Tested with PostgreSQL 17.10. - Bash. There are two binaries: - `go-service/contentd` serves the API and applies schema migrations. This is the production binary. - `harness/harness` creates dedicated test databases, loads test data, verifies behavior, and runs benchmarks. It is not needed in production. Build both from the repository root: ```sh bin/build ``` Both binaries default to the `assignment_harness_native` database. Configure the allowed HTTP hosts in your shell: ```sh export ALLOWED_HOSTS='127.0.0.1,localhost' ``` This connection uses a local PostgreSQL socket and your OS username. That PostgreSQL role must exist and have permission to create databases. For a different connection, set `PGHOST`, `PGPORT`, and `PGUSER`, or include them in `DATABASE_URL`: ```sh export DATABASE_URL='host=localhost port=5432 user=postgres dbname=assignment_harness_native' ``` Use a local `.pgpass` file with mode `0600` for passwords. Initialize the development database and load the small sample: ```sh harness/harness init-db go-service/contentd migrate harness/harness claim harness/harness fixture ``` All these commands use `DATABASE_URL` when set, otherwise the default database. `init-db` creates the database, `migrate` applies the service's embedded schema, and `claim` marks the empty database as owned by the test harness. Harness database commands require a dedicated database whose name starts with `assignment_harness_`; they refuse to take over an existing unmarked database automatically. Start the server: ```sh go-service/contentd ``` The server uses production settings by default, including health endpoints and host validation. In another terminal: ```sh curl http://127.0.0.1:18001/readyz curl http://127.0.0.1:18001/api/posts ``` Stop with Ctrl+C. To restart, set `ALLOWED_HOSTS` in your shell and run `go-service/contentd` again. No initialization or data loading is needed. Set `LISTEN_ADDR` to change the default `127.0.0.1:18001` bind address. Go development and operation do not require Python, uv, mise, or Docker. ## CLI help Every binary and `bin/` script supports `--help` and `-h`. Help exits without connecting to PostgreSQL, loading data, building binaries, or starting a server. ```sh go-service/contentd --help go-service/contentd migrate --help harness/harness --help harness/harness seed --help harness/harness bench --help bin/check --help ``` Harness command help lists only relevant flags, their defaults, an example, and whether the command replaces data or output files. `harness/harness help seed` also works. Override the default connection with `DATABASE_URL`, or pass `--db` to the harness explicitly. ## Fixture data Choose the dataset with the harness, using the same `DATABASE_URL` as the server: ```sh # Small contract-test sample: 3 users, 3 tags, 4 posts, 3 comments. # GET /api/posts returns the 3 published posts; the fourth is a draft. harness/harness fixture # Full synthetic dataset: 1,000 users, 50 tags, 100,000 posts, 500,000 comments. harness/harness seed ``` Run only the loader for the dataset you want. Both commands accept an explicit `--db 'dbname=assignment_harness_native'` instead of `DATABASE_URL`. Connection precedence is `--db` for the harness, then `DATABASE_URL`, then `dbname=assignment_harness_native`. An unset or empty `DATABASE_URL` uses the default. **Both loaders replace existing application data.** Use them only with owned, marked test databases. No server restart is needed after loading data. `/api/posts` is unpaginated, so the full dataset produces a large response. ## API | Method | Path | Description | | ------ | ---- | ----------- | | GET | `/api/posts` | Published posts, newest first | | GET | `/api/posts/search?q=` | Search across title and body | | GET | `/api/posts/by-tag/{slug}` | Posts carrying a given tag | | GET | `/api/posts/{id}` | Post detail with comments | | POST | `/api/posts` | Create a post | | POST | `/api/posts/{id}/comments` | Add a comment to a post | | GET | `/api/users/{id}` | User profile with post and comment counts | | GET | `/api/users/find?email=` | Look up a user by email | Interactive API docs are at , with the schema at `/api/openapi.json`. With the small fixture loaded, try these commands in another terminal: ```sh curl 'http://127.0.0.1:18001/api/posts/search?q=python' curl http://127.0.0.1:18001/api/posts/by-tag/python curl http://127.0.0.1:18001/api/users/1 curl 'http://127.0.0.1:18001/api/users/find?email=alice@example.com' curl -X POST http://127.0.0.1:18001/api/posts \ -H 'Content-Type: application/json' \ -d '{"author_id":1,"title":"My first post","body":"Hello from Go","tag_slugs":["python"]}' curl -X POST http://127.0.0.1:18001/api/posts/1/comments \ -H 'Content-Type: application/json' \ -d '{"author_id":2,"body":"A new comment"}' ``` Post creation requires `author_id`, `title`, and `body`; `tag_slugs` is optional. Authors and tags must already exist. The API has no user or tag creation endpoint. The full seed uses different users and titles from the small fixture. The implementation preserves the reference API's observed behavior, including known correctness limitations. Authentication and pagination are not implemented. ## Checks ```sh bin/check ``` The full check suite additionally requires Python 3.14, uv, and curl because it runs the Django reference and optimized Django control alongside Go. The inherited `mise.toml` can install Python and uv if you use mise; mise is optional. Checks include Go unit tests, race and static checks, the original smoke tests, migrations, production behavior, and 66 contract cases. They reset dedicated test databases and temporarily use ports 18000 and 18001, so stop the development server first. See [DEVELOPMENT.md](DEVELOPMENT.md) for details and benchmark commands. ## Troubleshooting | Symptom | Next step | | --- | --- | | Socket connection fails | Start PostgreSQL, or set `PGHOST` and `PGPORT` for your server. | | Role does not exist or peer authentication fails | Use a PostgreSQL role that exists and matches your connection's authentication rules; set `PGUSER` or `user=` in `DATABASE_URL`. | | Permission denied to create database | Ask the database administrator to create a dedicated database owned by your role, then use the manual setup in [DEVELOPMENT.md](DEVELOPMENT.md). | | Existing database is unmarked | Choose a fresh `assignment_harness_*` database name, or use the manual procedure for an empty database. | | Address already in use | Stop the previous server or set `LISTEN_ADDR=127.0.0.1:18002` and use that port in requests. | | `/readyz` returns 404 | Health endpoints require `APP_PROFILE=production`, the default; check that you have not set `APP_PROFILE=compat`. | ## Production operation `contentd` defaults to production mode. It requires `ALLOWED_HOSTS` and an explicitly migrated database. Set `DATABASE_URL` to your production database connection; otherwise it uses `assignment_harness_native`. Production mode adds health endpoints, request limits, structured logs, and generic error responses. For reference behavior tests only, start the same binary in compatibility mode: ```sh APP_PROFILE=compat go-service/contentd ``` Stop the previous server first. Compatibility mode preserves legacy error responses and does not expose health endpoints. Deployment uses the compiled `contentd` binary and PostgreSQL. Follow [deploy/README.md](deploy/README.md) for database roles, explicit migrations, environment configuration, the example systemd unit, shutdown, and rollback. The development setup and fixture loaders are not a production provisioning flow.