docs: add backend onboarding guide
This commit is contained in:
180
docs/ARCHITECTURE.md
Normal file
180
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Backend Architecture
|
||||
|
||||
This document explains the backend in beginner-friendly language.
|
||||
|
||||
## Big picture
|
||||
|
||||
```text
|
||||
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`.
|
||||
|
||||
1. `main.go` has this route:
|
||||
```go
|
||||
r.Get("/resources", handlers.ListResources)
|
||||
```
|
||||
2. The `/api` router has middleware:
|
||||
```go
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return handlers.WithPool(next, pool)
|
||||
})
|
||||
```
|
||||
This places the PostgreSQL pool into the request context.
|
||||
3. `handlers.ListResources` calls `poolFrom(r)` to get the DB pool.
|
||||
4. It reads query string filters (`q`, `type`, `language`, `category`, `sort`, `tag`).
|
||||
5. It builds SQL, queries PostgreSQL, scans rows into `ResourceDTO`.
|
||||
6. 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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```go
|
||||
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`, includes `admin_id` and email.
|
||||
- Wallet user token: issuer `ark-user`, includes wallet address and role `user`.
|
||||
|
||||
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:
|
||||
|
||||
```text
|
||||
S3_BUCKET non-empty + AWS config loads => upload to S3
|
||||
```
|
||||
|
||||
Otherwise:
|
||||
|
||||
```text
|
||||
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.sql` is not safe to run repeatedly on the same DB.
|
||||
- `JWT_SECRET` default is for dev only.
|
||||
- `RUN_WALLET_AUTH_SCHEMA=true` runs 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.
|
||||
Reference in New Issue
Block a user