id: task-316 title: Serve backlog images and static assets under /assets/backlog status: Done assignee:
- '@codex' created_date: '2025-11-11 14:24' updated_date: '2025-11-11 17:59' labels:
- server
- web
- assets dependencies: [] priority: medium
Description
Allow storing images and other static assets inside the project's backlog/assets directory and expose them when running the web UI so they can be referenced from tasks, documents and decisions.
Context
- Files should be reachable when the user opens
backlog browser. - Assets live anywhere under the
backlog/assetsdirectory (e.g.backlog/assets/images/foo.png,backlog/assets/uml/diagram.svg).
It will work if this image is displayed in the markdown preview AND when served from the backlog server:

Acceptance Criteria
- [x] #1 Files under the
backlog/assetsdirectory are reachable at/assets/<relative-path>whenbacklog browserruns. - [x] #2 Requests that attempt path traversal (contain
..) are rejected with 404. - [x] #3 Common image types (png/jpg/jpeg/gif/svg/webp/avif) are served with a correct Content-Type header.
- [x] #4 Missing files return 404; server errors return 500 and are logged.
- [x] #5 Documentation added explaining how to reference assets in Markdown and examples.
Implementation Notes
Goal
- Serve static files placed under
backlog/assetsat HTTP paths under/assets/<relative-path>whenbacklog browserruns.
High-level steps
-
Server: add route and handler
- Add a Bun route for
GET /assets/*insrc/server/index.ts. - Implement a handler
handleAssetRequest(req: Request)that:- Resolves the request path relative to
<projectRoot>/backlog/assets. - Decodes URL components and rejects paths containing
..immediately. - Derives backlog root via
this.core.filesystem.docsDirparent to avoid changing FileSystem API. - Uses
join(backlogAssetsRoot, relPath)and verifies the resolved path starts with the assets root. - Uses
Bun.file()to serve the file and sets Content-Type using a small extension-based map for common types (png/jpg/jpeg/gif/svg/webp/avif/pdf/txt/css/js). Falls back toapplication/octet-stream. - Returns 404 when the file does not exist, and 500 on errors (with logging).
- Resolves the request path relative to
- Add a Bun route for
-
Tests: functional use-cases
- Add tests under
src/test/server-assets.test.tsthat:- Create a test project dir using
createUniqueTestDirand callfilesystem.ensureBacklogStructure(). - Create
backlog/assets/images/test.pngandbacklog/assets/docs/readme.txtwith known contents. - Start
BacklogServer(TEST_DIR)on ephemeral portserver.start(0, false). - Verify
GET /assets/images/test.pngreturns 200, Content-Typeimage/png, and body content matches. - Verify
GET /assets/docs/readme.txtreturns 200, Content-Typetext/plain, and body content matches. - Verify missing file returns 404.
- Verify path traversal attempts (
/assets/../config.ymland encoded%2e%2e) return 404.
- Create a test project dir using
- Add tests under
-
CI / checks
- Ensure
npx biome check .passes (format imports and regexes) — run formatter if needed. - Tests require Bun runtime to run; document this in PR body and/or add GitHub Actions to run tests with Bun.
- Ensure
Edge cases & considerations
- Symbolic links: the handler uses path normalization; consider symlink resolution if repo allows symlinks under backlog/assets.
- Large files: consider streaming with proper headers; for now Bun.file uses efficient serving.
- Caching headers: consider
Cache-ControlorETagin follow-ups. - MIME map: use a small builtin map initially; optionally add
mimepackage later for full coverage.