Chirpy is a mini–Twitter clone built in Go. It provides a RESTful JSON API for user accounts, “chirps” (short text posts up to 140 characters), and a premium subscription (“Chirpy Red”) via a webhook. Under the hood it uses PostgreSQL (with sqlc for type-safe SQL queries), goose for schema migrations, JWT tokens for authentication, and Argon2id for password hashing. Chirps are automatically filtered for profanity, and only the author may delete a chirp. Administrators (in a “dev” platform mode) can reset the database and view simple metrics. The service also serves static files under /app/.
The high-level architecture is a single Go HTTP server behind a standard HTTP router. Clients (e.g. a web or mobile UI) make requests to endpoints like POST /api/users or GET /api/chirps. The server decodes requests, authenticates users via JWT, runs SQL queries against PostgreSQL, and returns JSON responses. Environment variables (in a .env file) configure the service, including the database URL and secret keys.
For example, Chirpy supports: user registration/login, JWT-based auth with refresh tokens, profile updates, creating/listing/deleting chirps (with profanity filtering), and a webhook endpoint to upgrade users to “Chirpy Red” premium membership. The service is instrumented with readiness (GET /api/healthz) and basic admin endpoints (GET /admin/metrics, POST /admin/reset in dev mode).
-
Prerequisites: Install Go (v1.24+), PostgreSQL, and command-line tools goose and sqlc. Also install
github.com/joho/godotenvfor loading.env(optional but recommended). Finally, get the required Go dependencies:go install github.com/pressly/goose/v3/cmd/goose@latest go install github.com/kyleconroy/sqlc/cmd/sqlc@latest go get github.com/joho/godotenv
-
Clone and build:
git clone <repo-url> Chirpy cd Chirpy go mod download
-
Environment variables: Create a
.envfile in the project root with at least the following variables (see [grainme/Chirpy README][64] for an example):DB_URL=postgres://user:password@localhost:5432/chirpydb?sslmode=disable JWT_SECRET=your-jwt-secret-key PLATFORM=dev POLKA_KEY=your-polka-api-key
DB_URL: PostgreSQL connection string (with?sslmode=disablein dev).JWT_SECRET: a long random string used to sign JWTs.PLATFORM: set todevfor development (enables the/admin/resetendpoint); useprodin production.POLKA_KEY: the secret API key expected on the Polka webhook (keep this truly secret).
(Note: The code uses
os.Getenv("SECRET")oros.Getenv("JWT_SECRET")for the JWT key depending on version; make sure it matches your code.) -
Database setup: Create the
chirpydbdatabase in Postgres. Run migrations using goose, for example:goose -dir sql/schema postgres "$DB_URL" upThis applies all pending schema migrations (goose will create a
schema_migrationstable). You can also step through, rollback, or check status with goose [as documented][51].A sample migration to add the “chirpy_red” column might look like:
-- +goose Up ALTER TABLE users ADD COLUMN is_chirpy_red BOOLEAN NOT NULL DEFAULT FALSE; -- +goose Down ALTER TABLE users DROP COLUMN is_chirpy_red;
(Each goose migration file in
sql/schema/should have-- +goose Upand-- +goose Downsections.) For details on goose commands, see the goose documentation or examples. -
SQLC codegen: The
.sqlquery files are insql/queries/. Whenever you edit SQL queries, runsqlc generateto re-generate Go query code ininternal/database. For example:sqlc generate
SQLC turns your SQL (marked with
-- name: <QueryName> :one|:many|:exec) into Go methods. Make sure each query has a proper name and correct return modifier (e.g.:onefor single-row results,:manyfor multiple rows,:execfor no-return DML). The generated code will expect the standard Postgres driver (github.com/lib/pq) and [google/uuid][23] if you use UUIDs in queries. -
Run the server: Start the API server on
:8080by:go run main.go
If all is well, you’ll see "Starting server on :8080". The health check endpoint
GET /api/healthzwill then respond with200 OK. -
Boot.dev CLI tests (optional): If you’re following the Boot.dev curriculum, install the Boot.dev CLI and run:
bootdev login # once, to authenticate bootdev run <test-id> # to run tests (debug mode) bootdev run <test-id> -s # to submit/check
This will execute predefined HTTP requests against your server. Use
runto debug errors; the-sflag will report pass/fail.
Chirpy is structured in packages:
-
main.go: Loads environment (viagodotenv), readsDB_URL,JWT_SECRET,PLATFORM,POLKA_KEYinto a configuration struct. It opens the Postgres connection (sql.Open("postgres", dbURL)) and initializes the generateddatabase.Queries. It then sets up HTTP routes onhttp.DefaultServeMuxand starts the server. For example (excerpt):godotenv.Load() cfg.Platform = os.Getenv("PLATFORM") cfg.Secret = os.Getenv("JWT_SECRET") // JWT signing key cfg.PolkaKey = os.Getenv("POLKA_KEY") // Polka webhook key cfg.DbURL = os.Getenv("DB_URL") db, err := sql.Open("postgres", cfg.DbURL) cfg.DbQueries = database.New(db) mux := http.NewServeMux() RegisterEndpoints(mux, &cfg) log.Println("Starting server on :8080") http.ListenAndServe(":8080", mux)
-
internal/auth/: Authentication helpers. This contains functions to hash and check passwords (using [Argon2id][73] for strong hashing) and to create/validate JWTs (usinggithub.com/golang-jwt/jwt/v5). It also has helper functions likeGetBearerToken(header http.Header)to parse theAuthorization: Bearer <token>header, andGetAPIKey(header http.Header)to parseAuthorization: ApiKey <key>. JWTs here include a custom claim for the user ID and expire in (typically) 1 hour. Upon each authenticated request,ValidateJWT(token, secret)checks the signature and expiry, returning the user’s UUID if valid. -
internal/database/: This wraps the code generated by sqlc. After runningsqlc generate, you get aQueriesstruct with methods likeCreateUser(ctx, params),GetUserByEmail,CreateChirp, etc. For example,dbQueries.CreateUser(ctx, database.CreateUserParams{Email, HashedPassword})will insert a new user and return a struct containing all columns (including the new UUID). Similarly, there are methods likeGetChirp,GetChirps,GetChirpsByAuthor,DeleteChirp,UpgradeUserToChirpyRed, etc., corresponding to the SQL queries defined. (SQLC ensures type safety – e.g. UUID columns becomeuuid.UUIDin Go, and missing or extra columns are compile-time errors.) -
sql/queries/: Contains.sqlfiles with the raw SQL statements. Each query begins with a comment like-- name: CreateUser :one(naming the Go method). The:onemeans one row is expected; use:manyfor multi-row (e.g.GetChirpsreturns multiple), or:execfor commands without a result set. Example (inuser.sql):-- name: CreateUser :one INSERT INTO users (email, hashed_password) VALUES ($1, $2) RETURNING *;
After editing these, run
sqlc generate. (Common pitfall: forgetting:oneor using the wrong return clause can cause mismatched results.) -
Handlers/Endpoints: Chirpy’s HTTP API routes are registered in
main.goor anapipackage. Key endpoints include:-
Users
POST /api/users– Create a new user. Request JSON:{"email": "...", "password": "..."}. Response:201 Createdwith JSON{"id": <uuid>, "email": "...", "is_chirpy_red": false, "created_at": "...", "updated_at": "..."}. (Theis_chirpy_redfield is added via a migration; it starts asfalseby default.)PUT /api/users– Update current user’s email/password. RequiresAuthorization: Bearer <token>. Request JSON:{"email": "...", "password": "..."}. Returns200 OKwith the updated user JSON (including the new timestamps andis_chirpy_redflag).
-
Auth
POST /api/login– User login. Request:{"email": "...", "password": "..."}. On success, returns200 OKwith JSON{"id": <uuid>, "email": "...", "is_chirpy_red": <bool>, "token": "<jwt>", "refresh_token": "<token>"}. Thetokenis a JWT (used in futureBearerheaders), andrefresh_tokenis a long random string stored server-side. If credentials fail, returns401 Unauthorized.POST /api/refresh– Refresh JWT. Requires headerAuthorization: Bearer <refresh_token>. If the refresh token is valid (found in the database and not expired), returns200 OKwith a new short-lived JWT:{"token": "<new_jwt>"}. If invalid, returns401.POST /api/revoke– Revoke refresh token. Requires headerAuthorization: Bearer <refresh_token>. This deletes the refresh token from the database, returning204 No Content.
-
Chirps (posts)
POST /api/chirps– Create a chirp (JWT required). Request:{"body":"Your message here"}. The body is auto-filtered for profanity (e.g. banned words like kerfuffle, sharbert, fornax are replaced with****) and must be ≤140 characters (else400 Bad Request). On success, returns201 Createdwith the chirp JSON:{"id": <uuid>, "body": "...", "user_id": <uuid>, "created_at": "...", "updated_at": "..."}.GET /api/chirps– List chirps. No auth required. Supports optional query paramsauthor_id=<user_uuid>(to filter by user) andsort=asc|desc(to sort by creation time). Ifauthor_idis given, the SQL queryGetChirpsByAuthoris used to fetch only that user’s chirps. OtherwiseGetChirpsreturns all chirps. The code then sorts them in memory byCreatedAt(ascending by default; descending ifsort=desc). (Filtering by author should be done in SQL for efficiency.) Returns200 OKwith JSON array of chirps (possibly empty). Example:curl "http://localhost:8080/api/chirps?author_id=<uuid>&sort=desc"GET /api/chirps/{chirpID}– Get one chirp by ID. No auth. Returns200 OKwith the chirp JSON, or404 Not Foundif the ID is invalid or missing.DELETE /api/chirps/{chirpID}– Delete a chirp. Requires JWT. First it looks up the chirp’s author from the DB; if the authenticated user ID doesn’t match, returns403 Forbidden. Otherwise it deletes the chirp and returns204 No Content.
-
Webhooks (Polka)
POST /api/polka/webhooks– Handle payment webhooks from Polka. This endpoint requires anAuthorization: ApiKey <key>header. If the key doesn’t match thePOLKA_KEY, it returns401 Unauthorized. The request body is JSON like{"event":"user.upgraded","data":{"user_id":"<uuid>"}}. Ifeventis anything other than"user.upgraded", the handler simply returns204 No Content(ignore it). If it is"user.upgraded", it parses theuser_idand calls the database query to set that user’sis_chirpy_red = true. On success it returns204 No Content; if the user isn’t found, it returns404 Not Found. This makes sure only Polka (who knows the API key) can upgrade users to premium..
-
Admin/Health
GET /api/healthz– Always returns200 OKwith a plaintext “OK” (readiness check).GET /admin/metrics– (Dev) shows a simple HTML page with visit counters (hits on the static file server).POST /admin/reset– (Dev only,PLATFORM=dev) wipes all users (and cascading chirps) and resets visit counters. Returns200 OK.
-
Each handler carefully sets the HTTP status codes and JSON fields as above. For example, creating a user returns JSON like:
POST /api/users
Content-Type: application/json
{"email":"walt@breakingbad.com","password":"123456"}HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "bbb39dc5-0ce4-4229-a8be-ddea5c7837a2",
"email": "walt@breakingbad.com",
"is_chirpy_red": false,
"created_at": "2026-06-18T02:54:39Z",
"updated_at": "2026-06-18T02:54:39Z"
}(Fields created_at, updated_at, and the UUID are automatically set in DB.) Login returns the same fields plus token and refresh_token. To delete a chirp:
curl -X DELETE http://localhost:8080/api/chirps/2d19eaeb-1d5d-409c-9709-3ab48c26b7f4 \
-H "Authorization: Bearer <your_jwt>"which yields 204 No Content. (The permissions check ensures only the chirp’s author can delete it.)
Chirpy uses JWT (JSON Web Tokens) for stateless authentication. Upon login, the server issues a signed JWT containing the user’s ID (and an expiration claim). The client must then include Authorization: Bearer <token> on protected requests. The server calls jwt.ParseWithClaims to verify signature and expiry using the shared secret (JWT_SECRET). A valid token means the user is authenticated as that ID. (Note: JWTs are signed but not encrypted; anyone can read their claims if they have the token. Thus, we never put sensitive data in a JWT – only user ID and expiry are stored.)
We also issue refresh tokens: long random strings stored (hashed) in the database with an expiry. The client gets one upon login and must use POST /api/refresh with Authorization: Bearer <refresh_token> to obtain a new JWT when the old one expires. This allows short-lived access tokens and the ability to revoke sessions. (If the refresh token is missing or invalid, 401 Unauthorized is returned.) Revoking simply deletes the token (via POST /api/revoke).
Passwords are hashed using Argon2id (via github.com/alexedwards/argon2id). This is a modern, slow, salted hashing algorithm (stronger than plain bcrypt). The code uses argon2id.CreateHash(password, params) to hash and argon2id.ComparePasswordAndHash to verify. Never store plain passwords.
For the Polka webhook, we use a separate API key mechanism: each request must include Authorization: ApiKey <POLKA_KEY>. The handler checks that header and rejects (401) if it’s wrong. This prevents unauthorized callers from hitting the webhook (only Polka knows the key).
SQL queries use parameter binding (via sqlc) so SQL injection is not a concern. Chirp text is sanitized by a simple word-filter (replacing banned words with asterisks), preventing obvious profanity. We also enforce chirp length ≤140 characters at the handler level.
Common security notes: use HTTPS in production to protect tokens in transit; keep all secrets (JWT_SECRET, POLKA_KEY, DB_URL credentials) out of source control (via .env or env vars). Don’t use debug or admin endpoints (like /admin/reset) in production. For further details on JWT best practices, see Boot.dev’s JWT guide.
Chirpy’s database schema starts with tables for users, chirps, and refresh_tokens. The users table has id (UUID PK), email, hashed_password, timestamps, and (after migration) is_chirpy_red BOOLEAN DEFAULT FALSE. chirps links to users via a foreign key with ON DELETE CASCADE, so deleting a user removes their chirps. The refresh_tokens table stores a token string, user_id (FK), and expiry.
We use Goose to version migrations. Each migration in sql/schema/ looks like:
-- +goose Up
ALTER TABLE users ADD COLUMN is_chirpy_red BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose Down
ALTER TABLE users DROP COLUMN is_chirpy_red;To apply all migrations, run:
goose -dir sql/schema postgres "$DB_URL" upThis will create/maintain a schema_migrations table and run any new scripts. You can also use goose down, goose status, etc. (The [DZone Goose tutorial][51] has a good Makefile example.) Keep migrations small and incremental.
Instead of writing raw SQL in code, we use sqlc to generate Go methods from .sql files. For example, in sql/queries/user.sql we might have:
-- name: GetUserByEmail :one
SELECT * FROM users WHERE email = $1;After sqlc generate, we can call dbQueries.GetUserByEmail(ctx, email), which returns a Go struct with typed fields. The :one suffix indicates exactly one row is expected. For multi-row queries (e.g. GetChirps), use :many. For commands like DELETE, use :exec. SQLC also supports named return values. See the [Boot.dev SQLC lesson][23] for examples: it notes that SQLC “generates Go code from SQL queries, making working with raw SQL easy and type-safe”.
Some tips:
- Prefix
sqlc generatein your build or CI so code stays up to date. - Include
UUIDcolumns (asuuid.UUIDin Go) by addinggo get github.com/google/uuid. - If you change the schema, write a goose migration and update your SQL queries accordingly (e.g. add the new column to RETURNING *).
- Don’t check
.envinto git; [23] suggests adding it to.gitignoreand usinggo getfor dependencies.
-
Registration:
POST /api/users– user submits email & password. The handler hashes the password (argon2id) and runsCreateUserin SQLC. The DB returns the new user (with UUID). We respond with JSON containingid,email,is_chirpy_red:false, etc. -
Login:
POST /api/login– user submits credentials. We look up the user by email (GetUserByEmail). If not found, we return401 Unauthorized. Otherwise we compare the hashed password (argon2id.ComparePasswordAndHash). If valid, we generate:- A JWT signed with
JWT_SECRET, containing the user’s ID. (This usesjwt-gounder the hood.) - A random refresh token (a long string, e.g. from
crypto/rand), which we store in DB with expiration (e.g. now+60 days).
We then return200 OKwith JSON:{"id":..., "email":..., "is_chirpy_red":..., "token": "<jwt>", "refresh_token": "<token>"}.
- A JWT signed with
-
Authenticated Requests: Subsequent requests must include
Authorization: Bearer <jwt>(orApiKeyfor Polka). Each handler calls a function likeValidateJWTwhich checks the signature and expiry. If invalid or expired, we return401. If valid, we obtain theuser_idfrom the JWT claims and proceed. -
Refresh: To extend a session, the client calls
POST /api/refreshwithAuthorization: Bearer <refresh_token>. The handler runs the SQLC queryGetUserFromRefreshToken(token). If a user is found and token is not expired, we issue a new JWT and return it. If not, we401 Unauthorized. -
Revoke: To log out,
POST /api/revokewith the refresh token header. The handler callsRevokeRefreshToken(token)(SQLDELETE). Returns204 No Content. -
Webhook (Polka): As noted,
POST /api/polka/webhooksexpects headerApiKey <POLKA_KEY>. On correct key, it parses JSON{"event":"...","data":...}. If event="user.upgraded", it extractsdata.user_id(a UUID) and runsUpgradeUserToChirpyRed(userID)in SQLC (which doesUPDATE users SET is_chirpy_red = true). Then returns204if user existed or404if not. For any other event, it immediately returns204(no error).
Chirpy includes automated CLI tests (if using Boot.dev). You can run them with the Boot.dev CLI (bootdev run <ID>). When a test fails, check the HTTP request/response it expects. Common issues:
-
401 Unauthorized: This means a required token/header is missing or invalid. E.g. ensure you use
Authorization: Bearer <JWT>on protected routes, andAuthorization: ApiKey <key>(note the word "ApiKey") on the Polka endpoint. A frequent mistake is forgetting the “Bearer ” prefix or misspelling the header. -
403 Forbidden: Occurs if a user tries to delete another’s chirp. This is correct behavior.
-
404 Not Found: Could be due to an incorrect path (e.g. missing trailing slash in registration), or an invalid UUID. Check your route registrations. For example,
mux.HandleFunc("/api/chirps/{chirpID}", ...)will not match"/api/chirps"– those are separate registrations. -
405 Method Not Allowed: Means the HTTP method isn’t handled. Ensure your handlers switch on
r.Methodor you’ve registered the correct method (see “Routing” below). -
500 Internal Error: Often a bug in server code. For instance, the SQLC query signature must match usage. If you get an “assignment mismatch” error (e.g. 2 variables but UpgradeUserToChirpyRed returns 1), it means your code called a function with the wrong number of return values. Check the generated method signature (maybe you used
:onevs:execincorrectly). Also check any JSON decoding errors.
Use tools like curl or Postman to replicate test requests. For example, to debug a Polka webhook test:
curl -v -X POST http://localhost:8080/api/polka/webhooks \
-H "Authorization: ApiKey wrongkey" \
-H "Content-Type: application/json" \
-d '{"event":"user.upgraded","data":{"user_id":"<uuid>"}}'This should give 401 (as expected if the key is wrong).
The Boot.dev CLI tests may show the expected status and actual status (as in the problem conversation above). Compare those to pinpoint issues.
In Go’s net/http, you can register handlers in two ways: method-specific routes or a single route with method switching. For example, one design is to do:
mux.HandleFunc("/api/users", func(w, r *http.Request) {
switch r.Method {
case http.MethodPost:
createUserHandler(w,r)
case http.MethodPut:
updateUserHandler(w,r)
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
})Alternatively, you can (with Go 1.20+ or using gorilla/mux) register different handlers per method/path combination. e.g.:
mux.HandleFunc("POST /api/users", createUserHandler)
mux.HandleFunc("PUT /api/users", updateUserHandler)Both achieve similar results. The switch approach keeps related logic together but can become verbose. The method-specific registration can be cleaner but requires either a multiplexer that supports it (or nesting muxes). Neither is “right” – it’s a design choice.
- Environment: In production, set
PLATFORM=prod(so admin reset is disabled). EnsureJWT_SECRETandPOLKA_KEYare securely provided (e.g. via container secrets or environment). Use a strong secret forJWT_SECRET(at least 256 bits). - Port: The server listens on
:8080by default. You can change this inmain.goor via an env var if needed. - HTTPS: Put Chirpy behind an HTTPS reverse proxy or load balancer in prod.
- Logging & Monitoring: Currently Chirpy prints to stdout on start; you may wish to add structured logging or metrics in a real setup.
- Scaling: The stateless Go server can be run behind a load balancer. PostgreSQL is a single point of truth (could use read replicas for scaling reads). Ensure proper indexing: the primary keys (user id, chirp id) are indexed by default. If you anticipate frequent filtering by
author_id, ensure it’s indexed (Postgres creates an index on foreign-key by default, and you may add indexes oncreated_atif sorting often). - CI/CD: Integrate
goosemigrations into your deployment pipeline. For example, see [51†L438-L445] on automatinggoose upin CI/CD.
- Why Go? Concurrency primitives, performance, static binaries, strong standard library.
- Why SQLC vs ORM? SQLC gives type-safe, explicit SQL without an ORM’s abstraction. It avoids runtime query mistakes by generating code at compile time. It’s easy to see exactly what SQL runs.
- Why JWT & Refresh? JWT allows stateless auth (no server-side session state). It scales well with microservices or multiple servers (no shared session store needed). Refresh tokens allow short-lived JWTs for security. Trade-off: JWTs can’t be easily revoked, hence we keep them short (1h) and use revokable refresh tokens. In interviews, note that JWT payloads shouldn’t contain secrets (they’re just identity claims).
- Routing Trade-offs: The code used net/http's default mux. For path parameters (like
/api/chirps/{id}), it usesr.URL.PathValue("chirpID")(Go 1.20+ supports{name}patterns) or regex. Discuss alternatives (gorilla/mux or manual regex switches). - DB Filtering vs In-Memory: The original assignment required that filtering by
author_idhappen in SQL. Indeed we useWHERE user_id = $1in the query (SQLC’sGetChirpsByAuthor). Sorting we do in Go usingsort.Slice, but we could also let Postgres sort viaORDER BY created_at. For large data, prefer pushing work to the DB (indexes, LIMIT/OFFSET for pagination, etc.). - Indexing: The
idcolumns are primary keys. Thechirps.user_idFK is implicitly indexed. If sorting or filtering by date often, adding an index oncreated_atmight help. - Security hardening: We hashed passwords and secret keys are env vars. For added security, rate limit login attempts to prevent brute force. Also verify JWT claims (we use standard
jwt.StandardClaims). - File Server: The code also serves static files under
/app/usinghttp.FileServer. We wrap it to count hits (metrics). In production, static assets are often served by a CDN or separate server.
Sample Interview Q&A:
- Q: How do you revoke a JWT before it expires? – We implement refresh tokens stored in DB; we can revoke by deleting the refresh token. The JWT itself is short-lived (1h) and not stored server-side (so truly stateless). One could also keep a token blacklist for full control.
- Q: How would you add pagination to GET /api/chirps? – We could extend the query with
LIMIT/OFFSETparameters (or better, use “cursor” pagination). SQLC supports dynamic queries or named parameters for this. Then clients could pass?limit=20&offset=40. - Q: Why use Argon2id for passwords? – It’s a modern, memory-hard hashing designed to resist GPU cracking, and Argon2id variant provides resistance against timing attacks. Compared to bcrypt, Argon2id has more tuning parameters. (Cite [64†L293-L300] which mentions Argon2id.)
- “404 Not Found” on POST /api/users: Ensure the path is correct. (No trailing slash or mismatched case.) Also check
HandleFuncregistrations: e.g. registering"/api/users"covers both POST and PUT in our switch. - “assignment mismatch” compiler error: Likely due to a SQLC query that doesn’t match your code. Maybe you wrote
-- name: X :onebut the query returns no rows, or vice versa. Check the SQLC-generated code or query annotation. - Login always fails (401): Check that the email exists and password hashing matches. Also ensure the DB was migrated/seeded with that user. Debug by attempting a known
CreateUserthen login. - “Token is expired”: The JWT issued has a 1-hour expiry. After that,
/api/loginis still required (or use the refresh token endpoint). - Polka webhook not working (401 instead of 204): The header must be exactly
Authorization: ApiKey <your-key>. It’s case-sensitive on “ApiKey” in our code. Also ensurePOLKA_KEYin.envmatches what you use. - SQL errors about uuid: We use
uuid.UUIDfields. Make sure you imported"github.com/google/uuid"and that your queries useuuid_generate_v4()orgen_random_uuid()(you may need thepgcryptoextension enabled forgen_random_uuid()as noted in Boot.dev [23†L125-L134]). In migration SQL usegen_random_uuid()in column defaults and enable the extension if needed.
If you run into any other issues, double-check environment names (JWT_SECRET vs SECRET), run goose up, and read server logs. The Boot.dev Discord/forums are also helpful for debugging questions.
- Official Go
net/httppackage documentation. - [Boot.dev Go SQLC Lesson][23] – explains sqlc queries,
.envusage, and scanning. - [Boot.dev JWT Blog][40] – covers JWT basics and flow.
- [Boot.dev JWT Review][41] – security considerations for JWT (immutability, no encryption).
- [Boot.dev CLI Docs][72] – using
bootdev runand CLI instructions. - SQLC official docs – for writing queries and regenerating code.
- Goose repository – for migration usage examples.
- Chirpy example code repositories (e.g. [neeeb1/chirpy][17] and [grainme/Chirpy][64]) for reference API usage.
- PostgreSQL docs on UUID generation and indexing.
(This README is meant to be exhaustive. Happy coding, and good luck with interviews!)
Sources: Chirpy code and related tutorials.