4.7 KiB
Backend Architecture
This document explains the backend in beginner-friendly language.
Big picture
Browser / Frontend
|
| HTTP JSON requests
v
cmd/server/main.go
|
+-- config.Load() reads environment variables
+-- db.Connect() creates PostgreSQL pool
+-- chi router maps URLs to handler functions
+-- handlers read/write DB and return JSON
|
v
PostgreSQL + local uploads or S3
The backend is one Go HTTP server. There is no framework magic: main.go wires everything manually.
Request flow example
Example: frontend calls GET /api/resources?limit=20.
main.gohas this route:r.Get("/resources", handlers.ListResources)- The
/apirouter has middleware:This places the PostgreSQL pool into the request context.r.Use(func(next http.Handler) http.Handler { return handlers.WithPool(next, pool) }) handlers.ListResourcescallspoolFrom(r)to get the DB pool.- It reads query string filters (
q,type,language,category,sort,tag). - It builds SQL, queries PostgreSQL, scans rows into
ResourceDTO. - It returns JSON using
writeJSON.
Why internal/?
In Go, a folder named internal can only be imported by code inside the same module tree. This prevents other modules from depending on private implementation packages.
This project uses:
internal/config private config package
internal/db private DB package
internal/auth private JWT package
internal/handlers private HTTP handler package
internal/seed private seed package
Main packages
cmd/server
cmd/server/main.go is the executable entry point.
Responsibilities:
- Load environment config.
- Connect to PostgreSQL.
- Ensure wallet nonce table when enabled.
- Create upload directory and placeholder file.
- Optionally seed the first admin.
- Configure optional S3 upload client.
- Register all HTTP middleware and routes.
- Start
http.ListenAndServe.
internal/config
config.Load() reads env vars and fills defaults. It returns a Config struct.
Important pattern:
addr := os.Getenv("HTTP_ADDR")
if addr == "" {
addr = ":8080"
}
This means env vars are optional in local development.
internal/db
db.Connect(ctx, databaseURL) parses DATABASE_URL, creates a pgx connection pool, then pings the DB.
The app shares this pool for all requests.
internal/auth
Two JWT token types exist:
- Admin token: issuer
ark-admin, includesadmin_idand email. - Wallet user token: issuer
ark-user, includes wallet address and roleuser.
Do not mix these. Admin middleware only accepts admin tokens.
internal/handlers
Handlers are grouped by topic:
public.go: public category/resource list/detail, search logs, counters.admin.go: admin login/dashboard/resource CRUD/tag replacement.wallet_auth.go: wallet nonce and signature verification.upload.go: admin file upload.middleware.go: admin auth middleware.context.go: stores DB pool in request context.util.go: shared JSON helpers.
Database model summary
Main tables from migrations/001_init.sql:
admins: admin email + bcrypt password hash.categories: resource categories with multilingual names.resources: library items; can be ppt/image/video/link/etc.tags: reusable tag names.resource_tags: many-to-many join table.search_logs: saved public search terms.
Wallet auth table from migration/startup DDL:
wallet_auth_nonces: one temporary nonce per wallet address.
Route groups
Public routes
These do not require auth:
- health:
/healthz,/health - categories/resources under
/api - wallet auth nonce/verify/me (me requires wallet JWT)
- resource view/download/share/favorite counters
Admin routes
POST /api/admin/login is public because it returns the admin token.
Everything in this group requires admin JWT:
- dashboard
- search logs
- resource list/get/create/update/delete
- upload
- admin categories list
Upload behavior
Admin upload endpoint: POST /api/admin/upload with multipart field file.
If S3 is configured:
S3_BUCKET non-empty + AWS config loads => upload to S3
Otherwise:
save file to UPLOAD_DIR and return /uploads/<filename>
The max upload size is 512 MiB (uploadMaxBytes).
Things to be careful with
migrations/001_init.sqlis not safe to run repeatedly on the same DB.JWT_SECRETdefault is for dev only.RUN_WALLET_AUTH_SCHEMA=trueruns DDL at startup. Use false if the runtime DB user should not create tables.- The app does not auto-load
.env. - Public resources are filtered to published + public; admin list sees all resources.