Files
FPMB/BUILD.md
2026-02-28 04:21:27 +00:00

21 KiB

FPMB — Build & Architecture Reference

This document is the authoritative reference for agentic coding sessions. Read it fully before making changes.


Quick-start commands

# 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:<name> 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/<teamID>/avatar.<ext>
│   ├── teams/<teamID>/banner.<ext>
│   ├── users/<userID>/avatar.<ext>
│   └── projects/<projectID>/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).

// Validate extension
allowedImageExts = map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}

// Build storage path
dir := fmt.Sprintf("../data/<resource>/%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("<coll>").UpdateOne(ctx, bson.M{"_id": id},
    bson.M{"$set": bson.M{"avatar_url": "/api/<resource>/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<T>(path, options?) JSON fetch, auto-refreshes token on 401, throws on error
apiFetchFormData<T>(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:

import { users, teams, projects, board, cards } from '$lib/api';

Auth store — src/lib/stores/auth.svelte.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:<name> tokens in Markdown with links. Unmatched refs render as `unknown file: <name>`.

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)

const updated = await users.uploadAvatar(file);   // returns updated User
authStore.setUser(updated);

const updatedTeam = await teams.uploadAvatar(teamId, file);  // returns updated Team

Display: show <img src={user.avatar_url}> 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/<teamID>/
│   ├── avatar.<ext>
│   └── banner.<ext>
├── users/<userID>/
│   └── avatar.<ext>
└── projects/<projectID>/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: <name> 🔄 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.