Both live in `apps/api/src/middleware/`. Use them; do not re-implement.
-`requireWorkspaceAccess(c, workspaceId)` (`middleware/auth.ts`) — verifies a workspace exists and the current user owns it. Returns the domain row. Throws 403 FORBIDDEN when the id is missing/empty or not owned, 404 NOT_FOUND when no such workspace exists. Call it at the top of every workspace-scoped route.
-`recordActivityForEntity({ actor, action, entityType, entityId, changes, workspaceId })` (`middleware/activity.ts`) — same as `recordActivity()` but resolves the workspace from the entity row when `workspaceId` is omitted. Unknown entity types or unresolvable entities are logged and skipped, never fatal to the request.
- CI runs on Gitea Actions (`.gitea/workflows/ci.yml`) on the self-hosted runner `projecte-runner`.
- Every push and pull request must pass the `quality` job: `bun install --frozen-lockfile` → `bun run typecheck` → web build (`cd apps/web && bun run build`) → `docker compose build`.
- A push to `main` (or a manual `workflow_dispatch`) triggers `deploy`, which runs `bash script/deploy.sh`. The `smoke` job then checks API health, SPA HTML, and login.
- Do not commit build artifacts (`.next/`, `dist/`, `.turbo/`, `*.tsbuildinfo`).
- Migrations live in `drizzle/` (0000–0005). Generate a new one with `bun run db:generate` after editing the schema.
-`bun run db:migrate` is the deploy-time migration: `db:sync` (`drizzle-kit push --force`) then `db:triggers` (`script/apply-triggers.ts`, search-vector triggers). Both are idempotent; safe to run on every deploy.