A minimal Twitter-like JSON API server built in Go. Chirpy supports user accounts, JWT-based authentication, refresh tokens, chirp creation and retrieval with filtering and sorting, secure deletion with ownership checks, and a webhook integration to upgrade users to "Chirpy Red" via an API key.
- User signup and login with Argon2id password hashing
- JWT access tokens + Refresh token flow
- Update user email and password (self only)
- Create, list, fetch, and delete chirps
- Filter by
author_id - Sort by
created_atascending or descending - Delete requires ownership
- Filter by
- Polka webhook (
ApiKeyauth) to upgrade a user to "Chirpy Red" - Admin utilities: metrics page and reset (dev only)
- Health check endpoint
- Go 1.21+
- Postgres
- sqlc for type-safe DB access (generated in
internal/database) - JWT (github.com/golang-jwt/jwt/v5)
- Argon2id (github.com/alexedwards/argon2id)
.
├── main.go
├── adminHandlers.go
├── chirpsHandler.go
├── userHandlers.go
├── refresh_tokenHandler.go
├── chirpy_red.go # Polka webhook handler
├── internal/
│ ├── auth/
│ │ ├── bearer_token.go
│ │ ├── jwt.go
│ │ ├── password_hash.go
│ │ ├── refresh_token.go
│ │ └── apikey.go
│ └── database/
│ ├── db.go
│ ├── models.go
│ ├── users.sql.go
│ ├── chirps.sql.go
│ ├── refresh_tokens.sql.go
│ └── custom.go # non-sqlc helpers (UpdateUser, DeleteChirp, etc.)
├── sql/
│ ├── schema/ # SQL migrations (up/down)
│ └── queries/ # sqlc query definitions
└── assets/, index.html # static files for /app/
- Go 1.21+
- Postgres 13+
PLATFORM=dev
DB_URL=postgres://<user>:<pass>@localhost:5432/chirpy?sslmode=disable
JWT_SECRET=replace-with-strong-secret
POLKA_KEY=replace-with-polka-webhook-key
Create the database and apply the schema in order of the files under sql/schema (or use your preferred migration tool):
-- From psql
CREATE DATABASE chirpy;
\c chirpy
-- Run each file in sql/schema in ascending numeric orderIf you use sqlc and modify sql/queries/*.sql, regenerate with:
sqlc generatego run ./...Server listens on :8080.
- Access token: JWT in
Authorization: Bearer <token>header, valid ~1 hour. - Refresh token: opaque string stored server-side; used to obtain a new access token via
POST /api/refresh. - Passwords: stored using Argon2id hashes.
- Webhook API key:
Authorization: ApiKey <KEY>must matchPOLKA_KEY.
Below, all bodies are JSON unless stated otherwise.
- GET
/api/healthz- 200 OK:
"OK"
- 200 OK:
- GET
/admin/metrics(HTML)- Shows file server hit count.
- POST
/admin/reset(dev only:PLATFORM=dev)- Resets metrics and deletes all users. 200 on success, 403 otherwise.
-
POST
/api/users- Request:
{ "email": "...", "password": "..." } - Response 201:
{ id, email, password, created_at, updated_at, is_chirpy_red }- Note:
passwordfield contains the hashed password as stored (for parity with current code paths).
- Note:
- 400 if invalid or already exists.
- Request:
-
POST
/api/login- Request:
{ "email": "...", "password": "..." } - Response 200:
{ id, email, created_at, updated_at, token, refresh_token, is_chirpy_red } - 401 if invalid credentials.
- Request:
-
PUT
/api/users(Authenticated)- Headers:
Authorization: Bearer <access_token> - Request:
{ "email": "...", "password": "..." }(update own account) - Response 200:
{ id, email, created_at, updated_at, is_chirpy_red } - 401 for missing/invalid token, 400 for bad input.
- Headers:
-
POST
/api/refresh- Headers:
Authorization: Bearer <refresh_token> - Response 200:
{ "token": "<new_access_token>" } - 401 if missing/malformed header, invalid/expired token.
- Headers:
-
POST
/api/revoke- Headers:
Authorization: Bearer <refresh_token> - Response 204 on success.
- 401 if missing or invalid refresh token.
- Headers:
-
POST
/api/chirps(Authenticated)- Headers:
Authorization: Bearer <access_token> - Request:
{ "body": "<up to 140 chars>" }- Banned words (case-insensitive) are censored:
kerfuffle,sharbert,fornax.
- Banned words (case-insensitive) are censored:
- Response 201:
{ id, body, created_at, updated_at, user_id } - 400 if invalid; 401 if token missing/invalid.
- Headers:
-
GET
/api/chirps- Query params:
author_id(optional, UUID): filter to a single author's chirpssort(optional):ascordescbycreated_at(defaultasc)
- Response 200:
[{ id, body, created_at, updated_at, user_id }, ...]
- Query params:
-
GET
/api/chirps/{chirpID}- Response 200:
{ id, body, created_at, updated_at, user_id } - 404 if not found; 400 if invalid ID.
- Response 200:
-
DELETE
/api/chirps/{chirpID}(Authenticated)- Headers:
Authorization: Bearer <access_token> - Only the author of the chirp can delete it.
- 204 on success; 404 if not found; 403 if not author; 401 if unauthorized.
- Headers:
- POST
/api/polka/webhooks- Headers:
Authorization: ApiKey <POLKA_KEY> - Body example (only
user.upgradedis acted upon):{ "event": "user.upgraded", "data": { "user_id": "<uuid>" } } - Behavior: If key matches and event is
user.upgraded, setsis_chirpy_red = truefor the user. Responds204on success.401if invalid key.
- Headers:
Signup:
curl -s -X POST http://localhost:8080/api/users \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"pass1234"}' | jqLogin:
LOGIN=$(curl -s -X POST http://localhost:8080/api/login \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"pass1234"}')
TOKEN=$(echo "$LOGIN" | jq -r .token)
REFRESH=$(echo "$LOGIN" | jq -r .refresh_token)Create a chirp:
curl -s -X POST http://localhost:8080/api/chirps \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"body":"My first chirp!"}' | jqList chirps (author + order):
curl -s "http://localhost:8080/api/chirps?author_id=<AUTHOR_UUID>&sort=desc" | jqDelete a chirp:
curl -i -X DELETE "http://localhost:8080/api/chirps/<CHIRP_UUID>" \
-H "Authorization: Bearer $TOKEN"Refresh access token:
curl -s -X POST http://localhost:8080/api/refresh \
-H "Authorization: Bearer $REFRESH" | jqRevoke refresh token:
curl -i -X POST http://localhost:8080/api/revoke \
-H "Authorization: Bearer $REFRESH"Webhook upgrade:
curl -i -X POST http://localhost:8080/api/polka/webhooks \
-H "Authorization: ApiKey $POLKA_KEY" \
-H 'Content-Type: application/json' \
-d '{"event":"user.upgraded","data":{"user_id":"<USER_UUID>"}}'- Run auth unit tests:
go test ./internal/auth -v- Run all tests:
go test ./... -vDELETE /api/chirps/{id}checks ownership via the JWTsubclaim.POST /admin/resetis available only whenPLATFORM=dev.- Responses use standard HTTP codes and JSON error payloads (e.g.,
{ "error": "..." }). - For production, ensure strong
JWT_SECRET, secure DB connection, and rotatePOLKA_KEYas needed.