# FPMB — Build & Architecture Reference This document is the authoritative reference for agentic coding sessions. Read it fully before making changes. --- ## Quick-start commands ```bash # Frontend dev server bun run dev # Frontend production build (source of truth for TS errors) bun run build # Backend run (from server/) go run ./cmd/api/main.go # Backend compile check (source of truth for Go errors) go build ./... ``` - Go binary: `/usr/bin/go` (1.22.2) - Package manager: `bun` (not npm/pnpm/yarn) - Go module path: `github.com/fpmb/server` - Backend must be run from `server/` — relative paths like `../data/` resolve from there --- ## Repository layout ``` openboard/ ├── src/ SvelteKit frontend (static SPA adapter) │ ├── lib/ │ │ ├── api/ │ │ │ ├── client.ts apiFetch, apiFetchFormData, token management │ │ │ └── index.ts all typed API methods grouped by resource │ │ ├── components/ │ │ │ ├── Markdown/ Markdown.svelte — renders marked + DOMPurify │ │ │ └── Modal/ Modal.svelte — generic overlay │ │ ├── stores/ │ │ │ └── auth.svelte.ts authStore singleton (init, login, register, logout, setUser) │ │ ├── types/ │ │ │ └── api.ts TypeScript interfaces mirroring Go models │ │ └── utils/ │ │ └── fileRefs.ts resolveFileRefs() — converts $file: refs in Markdown │ └── routes/ │ ├── (auth)/ login, register pages (no auth guard) │ └── (app)/ │ ├── +layout.svelte auth guard, top navbar (avatar, logout button) │ ├── +page.svelte dashboard │ ├── board/[id]/ Kanban board │ ├── calendar/ month/week calendar │ ├── notifications/ notification inbox │ ├── projects/ project list + project settings │ ├── settings/user/ user profile + avatar upload + password change │ ├── team/[id]/ team overview + team settings (avatar/banner upload) │ └── whiteboard/[id]/ canvas whiteboard ├── server/ │ ├── cmd/api/main.go Fiber app bootstrap, all route registrations │ └── internal/ │ ├── database/db.go MongoDB connection, GetCollection helper │ ├── handlers/ one file per resource group (auth, teams, projects, …) │ ├── middleware/auth.go JWT Protected() middleware │ ├── models/models.go all MongoDB document structs (source of truth for field names) │ ├── routes/ (unused legacy dir — ignore) │ └── utils/ shared Go utilities ├── static/ fonts, favicon ├── data/ runtime file storage (gitignored) │ ├── teams//avatar. │ ├── teams//banner. │ ├── users//avatar. │ └── projects//files/… ├── build/ SvelteKit production output (served by Go) ├── package.json ├── svelte.config.js ├── vite.config.ts └── tsconfig.json ``` --- ## Backend (Go + GoFiber v2) ### Route registration — `server/cmd/api/main.go` All routes are registered here. Add new routes to the appropriate group. Every group except `/auth` is wrapped in `middleware.Protected()`. ``` /api/health GET — liveness check /api/auth/register POST /api/auth/login POST /api/auth/refresh POST /api/auth/logout POST (Protected) /api/users/me GET PUT /api/users/me/password PUT /api/users/me/avatar POST GET (multipart upload / serve) /api/users/me/files GET /api/users/me/files/folder POST /api/users/me/files/upload POST /api/users/search GET ?q= /api/teams GET POST /api/teams/:teamId GET PUT DELETE /api/teams/:teamId/members GET /api/teams/:teamId/members/invite POST /api/teams/:teamId/members/:userId PUT DELETE /api/teams/:teamId/projects GET POST /api/teams/:teamId/events GET POST /api/teams/:teamId/docs GET POST /api/teams/:teamId/files GET /api/teams/:teamId/files/folder POST /api/teams/:teamId/files/upload POST /api/teams/:teamId/avatar POST GET /api/teams/:teamId/banner POST GET /api/projects GET POST /api/projects/:projectId GET PUT DELETE /api/projects/:projectId/archive PUT /api/projects/:projectId/members GET POST /api/projects/:projectId/members/:userId PUT DELETE /api/projects/:projectId/board GET /api/projects/:projectId/columns POST /api/projects/:projectId/columns/:columnId PUT DELETE /api/projects/:projectId/columns/:columnId/position PUT /api/projects/:projectId/columns/:columnId/cards POST /api/projects/:projectId/events GET POST /api/projects/:projectId/files GET /api/projects/:projectId/files/folder POST /api/projects/:projectId/files/upload POST /api/projects/:projectId/webhooks GET POST /api/projects/:projectId/whiteboard GET PUT /api/cards/:cardId PUT DELETE /api/cards/:cardId/move PUT /api/events/:eventId PUT DELETE /api/notifications GET /api/notifications/read-all PUT /api/notifications/:notifId/read PUT /api/notifications/:notifId DELETE /api/docs/:docId GET PUT DELETE /api/files/:fileId/download GET /api/files/:fileId DELETE /api/webhooks/:webhookId PUT DELETE /api/webhooks/:webhookId/toggle PUT ``` ### MongoDB models — `server/internal/models/models.go` This file is the single source of truth for all field names and types. Always check here before referencing a field. | Struct | Collection | Key fields | |---|---|---| | `User` | `users` | `_id`, `name`, `email`, `password_hash` (json:`-`), `avatar_url` | | `Team` | `teams` | `_id`, `name`, `workspace_id`, `avatar_url`, `banner_url`, `created_by` | | `TeamMember` | `team_members` | `team_id`, `user_id`, `role_flags`, `invited_by` | | `Project` | `projects` | `_id`, `team_id`, `name`, `description`, `visibility`, `is_public`, `is_archived`, `created_by` | | `ProjectMember` | `project_members` | `project_id`, `user_id`, `role_flags` | | `BoardColumn` | `columns` | `project_id`, `title`, `position` | | `Card` | `cards` | `column_id`, `project_id`, `title`, `description`, `priority`, `color`, `due_date`, `assignees []string`, `subtasks []Subtask`, `position` | | `Subtask` | (embedded) | `id int`, `text`, `done` | | `Event` | `events` | `title`, `date`, `time`, `color`, `description`, `scope`, `scope_id` | | `Notification` | `notifications` | `user_id`, `type`, `message`, `project_id`, `card_id`, `read` | | `Doc` | `docs` | `team_id`, `title`, `content`, `created_by` | | `File` | `files` | `project_id`, `team_id`, `user_id`, `name`, `type`, `size_bytes`, `parent_id`, `storage_url` | | `Webhook` | `webhooks` | `project_id`, `name`, `type`, `url`, `secret_hash` (json:`-`), `status`, `last_triggered` | | `Whiteboard` | `whiteboards` | `project_id`, `data` | ### RBAC Roles are **hierarchical integers**, not bitflags. Use `>=` comparisons. ``` Viewer = 1 Editor = 2 Admin = 4 Owner = 8 ``` Example: `member.RoleFlags >= 2` means Editor or above. ### Image upload/serve pattern (handler) Reference implementation: `server/internal/handlers/teams.go` (UploadTeamAvatar / ServeTeamAvatar). ```go // Validate extension allowedImageExts = map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true} // Build storage path dir := fmt.Sprintf("../data//%s", id.Hex()) os.MkdirAll(dir, 0755) // Delete old file (glob by base name, any extension) old, _ := filepath.Glob(filepath.Join(dir, "avatar.*")) for _, f := range old { os.Remove(f) } // Save new file c.SaveFile(fh, filepath.Join(dir, "avatar"+ext)) // Update DB with static URL string database.GetCollection("").UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": bson.M{"avatar_url": "/api//avatar", "updated_at": time.Now()}}) // Serve matches, _ := filepath.Glob(filepath.Join(dir, "avatar.*")) return c.SendFile(matches[0]) ``` Key points: - `allowedImageExts` is declared once in `teams.go` and reused across the `handlers` package (same package, no redeclaration needed in `users.go`) - Avatar URL stored in DB is a **static string** (e.g. `/api/users/me/avatar`), not per-extension — the serve endpoint globs at runtime - Max upload size: GoFiber default (4MB). Adjust `fiber.Config{BodyLimit}` in `main.go` if needed ### Background jobs `startDueDateReminder()` in `main.go` runs every hour and creates `due_soon` notifications for cards due within 1 or 3 days. It deduplicates within a 24-hour window. --- ## Frontend (SvelteKit + Svelte 5 Runes) ### Rules — non-negotiable - **Svelte 5 Runes only**: `$state`, `$derived`, `$props`, `$bindable`. `onMount` is also acceptable. - **No legacy Svelte**: no `export let`, no reactive `$:`, no `writable` stores. - `$state` may only be used inside `.svelte`, `.svelte.ts`, or `.svelte.js` files — never in plain `.ts` modules. - No code comments anywhere. - No extra files or folders. - `$page.params.id` must always be written as `$page.params.id ?? ''`. ### LSP / type errors The LSP cache is often stale. **Do not** treat LSP errors as real until confirmed by `bun run build`. Pre-existing LSP errors on `$page`, `Project.visibility`, `Project.is_public`, etc. are known stale artefacts. ### API client — `src/lib/api/client.ts` | Export | Purpose | |---|---| | `apiFetch(path, options?)` | JSON fetch, auto-refreshes token on 401, throws on error | | `apiFetchFormData(path, formData)` | Multipart POST (method hardcoded to POST), same token/retry logic | | `getAccessToken()` | Returns current in-memory access token | | `setAccessToken(token)` | Updates in-memory token and localStorage | Tokens: - `access_token` — localStorage + in-memory, 15-minute JWT - `refresh_token` — localStorage only, 7-day JWT - `user_id` — localStorage only (used for convenience reads) ### API methods — `src/lib/api/index.ts` All API calls are grouped by resource: `auth`, `users`, `teams`, `projects`, `board`, `cards`, `events`, `notifications`, `docs`, `files`, `webhooks`. Import the group you need: ```ts import { users, teams, projects, board, cards } from '$lib/api'; ``` ### Auth store — `src/lib/stores/auth.svelte.ts` ```ts authStore.user // User | null (reactive) authStore.loading // boolean (reactive) authStore.init() // call once in root layout onMount authStore.login(email, password) authStore.register(name, email, password) authStore.logout() // calls API, clears tokens, nulls user authStore.setUser(u) // update user after profile/avatar changes ``` ### TypeScript interfaces — `src/lib/types/api.ts` Mirrors Go models exactly. Always import from here; never inline interfaces. Key interfaces: `User`, `Team`, `TeamMember`, `Project`, `ProjectMember`, `Card`, `Subtask`, `Column`, `BoardData`, `Event`, `Notification`, `Doc`, `FileItem`, `Webhook`, `Whiteboard`, `AuthResponse`. ### File ref resolution — `src/lib/utils/fileRefs.ts` `resolveFileRefs(text, files)` replaces `$file:` tokens in Markdown with links. Unmatched refs render as `` `unknown file: ` ``. ### Tailwind notes - Use Tailwind v4 utility classes only. - `ring-neutral-750` does not exist — use `ring-neutral-800`. - Never combine `inline-block` with `flex` on the same element — `inline-block` wins and kills flex behaviour. Use `flex` alone. --- ## Common patterns ### Adding a new API route (end-to-end checklist) 1. Add handler function in the appropriate `server/internal/handlers/*.go` file. 2. Register the route in `server/cmd/api/main.go` under the right group. 3. Add the typed method to `src/lib/api/index.ts`. 4. If a new response shape is needed, add an interface to `src/lib/types/api.ts`. 5. Call from a `.svelte` page; use `apiFetchFormData` for multipart, `apiFetch` for everything else. 6. Verify: `go build ./...` from `server/` and `bun run build` from root. ### Archived project read-only enforcement `src/routes/(app)/board/[id]/+page.svelte`: - `isArchived` state derived from `project.is_archived` on mount. - Yellow banner shown when archived. - Cards: `draggable={!isArchived}`, click/keydown handlers suppressed, edit button hidden. - Add Card and Add Column buttons hidden when archived. - `handleDrop` returns early when `isArchived`. ### User / team avatar upload (frontend) ```ts const updated = await users.uploadAvatar(file); // returns updated User authStore.setUser(updated); const updatedTeam = await teams.uploadAvatar(teamId, file); // returns updated Team ``` Display: show `` if `avatar_url` is set, otherwise show the user's initial letter in a coloured circle. --- ## Environment variables | Variable | Default | Description | |---|---|---| | `PORT` | `8080` | Server listen port | | `MONGO_URI` | `mongodb://localhost:27017` | MongoDB connection string | | `MONGO_DB_NAME` | `fpmb` | MongoDB database name | | `JWT_SECRET` | — | Required in production | `.env` is loaded automatically via `godotenv` in `main.go`. System env vars are used as fallback. --- ## Data directory layout Runtime files are stored under `data/` relative to the repository root. The Go server runs from `server/`, so it accesses `../data/`. ``` data/ ├── teams// │ ├── avatar. │ └── banner. ├── users// │ └── avatar. └── projects//files/… ``` `data/` is gitignored. Goal Build out and improve the FPMB (Free Project Management Boards) self-hosted project management app at /home/coder/openboard. The current session focused on: user avatar upload, logout button, archived project read-only mode, task card UI fixes, and $file: unknown file handling. The final task (still in progress) is updating README.md and creating BUILD.md with full coding/architecture details for future agent sessions. --- Instructions - Project is at /home/coder/openboard - No code comments - No extra files or folders - Frontend: Svelte 5 Runes only ($state, $derived, $props, $bindable) — onMount is acceptable; no legacy Svelte patterns - Backend: Go, module path github.com/fpmb/server - Use bun (not npm): bun run build, bun run dev - Go is at /usr/bin/go (1.22.2); run from server/ via go run ./cmd/api/main.go - LSP has a stale cache — use bun run build / go build ./... as the source of truth for real errors; ignore LSP errors that were pre-existing - $page.params.id must use ?? '' - RBAC: Viewer=1, Editor=2, Admin=4, Owner=8 — roles are hierarchical (>= not bitwise) - Server runs from server/ so ../data/ resolves to /home/coder/openboard/data/ - File storage: ../data/teams//avatar., ../data/users//avatar. - Use apiFetchFormData in frontend for multipart uploads - $state may only be used inside .svelte or .svelte.ts/.svelte.js files, not plain .ts modules --- Discoveries Architecture - allowedImageExts map is defined in server/internal/handlers/teams.go — shared across the handlers package, so users.go reuses it directly (same package, no redeclaration) - authStore (src/lib/stores/auth.svelte.ts) already has a logout() method that calls the API, clears tokens, and nulls the user — no need to duplicate logic - apiFetchFormData always uses POST method (hardcoded in client.ts) - LSP errors on board page ($page, Project.visibility, Project.is_public, etc.) are all pre-existing stale cache issues — builds pass cleanly - ring-neutral-750 is not a valid Tailwind class (causes white ring fallback); use ring-neutral-800 for card backgrounds - inline-block + flex conflict — inline-block wins and kills flex centering; use flex alone - $file: refs are resolved in src/lib/utils/fileRefs.ts via resolveFileRefs() — unmatched refs previously returned raw `$file:name` syntax - Board page is_archived enforcement: drag-drop guarded in handleDrop, template uses {#if !isArchived} to hide Add Card / Add Column, draggable={!isArchived} on cards Key patterns - Team/user image upload: validate ext with allowedImageExts, glob-delete old file, SaveFile, UpdateOne with URL, return updated document - Serve image: glob-find by extension, SendFile - Avatar URL stored as /api/users/me/avatar (static string, not per-extension) — the serve endpoint finds the file at runtime --- Accomplished ✅ Completed this session 1. User avatar upload — Full end-to-end: - server/internal/handlers/users.go: Added UploadUserAvatar and ServeUserAvatar handlers - server/cmd/api/main.go: Registered POST /users/me/avatar and GET /users/me/avatar - src/lib/api/index.ts: Added users.uploadAvatar(file) - src/routes/(app)/settings/user/+page.svelte: Added avatar display ( if set, else initial letter), file input upload button, success/error feedback, calls authStore.setUser(updated) 2. Logout button — Added to top navbar in src/routes/(app)/+layout.svelte: - Arrow-out icon button to the right of the user avatar (desktop only, hidden md:flex) - Calls authStore.logout() then goto('/login') 3. Archived project read-only mode — src/routes/(app)/board/[id]/+page.svelte: - Added isArchived state, populated from project.is_archived on mount - Yellow archived banner shown when isArchived is true - Cards: draggable={!isArchived}, click/keydown suppressed, edit ... button hidden - "Add Card" button hidden per column when archived - "Add Column" button hidden when archived - handleDrop returns early when isArchived 4. Task card assignee ring fix — ring-neutral-750 → ring-neutral-800, inline-block removed (was conflicting with flex centering) 5. $file: unknown file fallback — src/lib/utils/fileRefs.ts: unmatched refs now render as `unknown file: ` 🔄 In Progress — Documentation update - README.md — needs updating to reflect: user avatar upload, team avatar/banner, logout button, archived read-only mode, new API routes - BUILD.md — needs to be created as a comprehensive reference for future agentic coding sessions, covering all architecture, conventions, patterns, file layout, API routes, models, and coding rules --- Relevant files / directories Backend (Go) server/ ├── cmd/api/main.go routes registration (recently: user avatar routes added) └── internal/ ├── handlers/ │ ├── auth.go │ ├── teams.go reference: allowedImageExts, uploadTeamImage/serveTeamImage pattern │ ├── users.go recently added: UploadUserAvatar, ServeUserAvatar │ ├── projects.go │ ├── board.go │ ├── cards.go │ ├── files.go │ ├── notifications.go │ ├── docs.go │ ├── events.go │ ├── webhooks.go │ └── whiteboard.go ├── middleware/ │ └── auth.go ├── models/ │ └── models.go source of truth for all field names/types └── database/ └── db.go Frontend src/ ├── lib/ │ ├── api/ │ │ ├── client.ts apiFetch, apiFetchFormData, token management │ │ └── index.ts all API methods (recently: users.uploadAvatar added) │ ├── components/ │ │ ├── Markdown/Markdown.svelte │ │ └── Modal/Modal.svelte │ ├── stores/ │ │ └── auth.svelte.ts authStore: init, login, register, logout, setUser │ ├── types/ │ │ └── api.ts all TypeScript interfaces │ └── utils/ │ └── fileRefs.ts resolveFileRefs (recently: unknown file fallback fixed) └── routes/ └── (app)/ ├── +layout.svelte navbar, auth guard (recently: logout button added) ├── board/[id]/+page.svelte kanban board (recently: archived read-only mode) ├── settings/user/+page.svelte user settings (recently: avatar upload UI) ├── team/[id]/ │ ├── +page.svelte team overview (shows avatar/banner) │ └── settings/+page.svelte team settings (avatar/banner upload) └── projects/ ├── +page.svelte project list (shows is_archived badge) └── [id]/settings/+page.svelte project settings (archive/delete) Docs (in progress) /home/coder/openboard/README.md needs update /home/coder/openboard/BUILD.md needs to be created Data directory (runtime) /home/coder/openboard/data/ ├── teams//avatar. ├── teams//banner. └── users//avatar.