6.3 KiB
DECISIONS.md — Project Bifrost
Decisions made during autonomous build. Each entry: what was chosen, why, and where it applies.
D-01 · Content collections in src/content/ not root content/
Chose: src/content/updates/ and src/content/meetings/ for Astro content collections.
Why: Astro 4 requires content collections to live inside src/content/. The root content/ folder cannot be used for typed collections with Zod schemas.
Applies to: updates, meetings.
Note: content/roadmap.md stays at root and is read via fs.readFileSync since it's a single file, not a collection.
D-02 · marked added as a runtime dependency
Chose: Added marked ^12.0.0 for rendering user-contributed markdown.
Why: SPEC requires markdown-lite rendering (bold, italic, links, lists, code blocks) in contributions and replies. A homegrown parser risks XSS and correctness bugs. marked is tiny, well-maintained, and ships its own TypeScript types.
Note: HTML output is not sanitized (no DOMPurify). Acceptable for a private hub with 14 trusted users. Flag for v1.1 if scope expands.
D-03 · Sessions: 7-day, random 32-byte hex ID
Chose: Sessions stored in SQLite. Cookie bifrost_session holds a 32-byte random hex string. Expiry 7 days. HttpOnly, SameSite=Lax.
Why: Simple, auditable. No JWTs — session validity is always checkable server-side. SPEC mandates HttpOnly + SameSite=Lax.
D-04 · Invite tokens: HMAC-signed, hash stored in DB
Chose: Token format ${randomBase64url}.${hmac16chars}. SHA-256 hash of the full token stored in invites.token_hash. HMAC key = BIFROST_SECRET.
Why: SPEC says "HMAC-signed, not JWTs". Storing the hash means a compromised DB doesn't reveal usable tokens.
D-05 · BIFROST_SECRET env var with dev fallback
Chose: process.env.BIFROST_SECRET ?? 'dev-secret-do-not-use-in-production'
Why: Zero-config for local dev. Production must set the env var. A .env.example documents it.
D-06 · Calendar navigation via URL params, no JS
Chose: /calendar?y=2026&m=4 — month grid built server-side per request.
Why: Works without JavaScript. Simpler to reason about. JS keyboard nav is a v1.1 enhancement.
D-07 · Reactions use form POST (full reload)
Chose: +1 reaction is a plain <form method="post">. Full page reload.
Why: No JS required. Works in all contexts. AJAX reactions are a UX polish item for v1.1.
Trade-off: Page reload loses scroll position. Acceptable for prototype.
D-08 · Roadmap parsed from content/roadmap.md with simple section splitter
Chose: Read content/roadmap.md with fs.readFileSync, split on ## headings, render each section's items with marked.
Why: It's a single file, not a collection. No need to add Astro content collection overhead for one file.
D-09 · better-sqlite3 excluded from Vite optimisation
Chose: Added vite.ssr.external: ['better-sqlite3'] and vite.optimizeDeps.exclude.
Why: Prevents Vite from attempting to bundle the native Node module, which would fail. Standard pattern for native modules in Vite/Astro.
D-10 · Ghost border on form field bottom edges
Chose: Input fields use border-bottom: 1px solid rgba(186,186,176,0.30) (ghost border pattern from design system).
Why: This is explicitly permitted by the design system README for form fields ("bottom-only Ghost Border"). Not a structural layout border.
D-11 · AppLayout for authenticated pages, BaseLayout for auth pages
Chose: Two layouts. AppLayout.astro has the glass nav, user info, and nav links. BaseLayout.astro (existing) is used for login and invite redemption pages.
Why: Auth pages should not show the nav — they are entry points before identity is established.
D-12 · Attendance RSVP shown only to Fenja role
Chose: The attendance tally on meeting pages is visible to all (yes/no counts). Individual RSVP selections are visible only to fenja-role users. Why: SPEC §5.4: "A simple tally, not a hard RSVP. Shown only to Fenja."
D-13 · Contribution edit window: 10 min enforced server-side
Chose: Edit button visible client-side based on data-created timestamp. Server validates the 10-minute window on POST.
Why: Both checks needed: client for UX, server for security. The client check is just convenience.
D-14 · Password reset via new invite link (no email flow)
Chose: No forgot-password page built. Admin issues a new invite link from /admin. Why: SPEC §3 explicitly says: "No forgot-password flow for v1 — admin re-issues invite links."
D-15 · Join CTA uses fetch + in-place DOM swap (not full-page POST)
Chose: The "I want to join" button fires fetch('/api/join', { method: 'POST' }) and swaps the CTA element in-place on success. The server-rendered state (alreadyJoined) handles the persistent confirmation on subsequent visits.
Why: The brief specified "no navigation, replaces page content in-place". Full-page POST would cause a navigation. The server-side fallback ensures correctness without JS.
D-16 · Innofounder logo: file-existence check at render time
Chose: existsSync('public/innofounder-logo.svg') at SSR render time. If missing, shows a placeholder rectangle with instructions. If present, renders the img tag.
Why: The brief said to show a placeholder if the file doesn't exist, and not to invent or regenerate the logo. SSR file check is the simplest approach that works without a build step.
D-17 · Architecture diagram is HTML/CSS using surface tiers, not SVG
Chose: Four div.arch-row elements with inline style="background: var(--surface-tier)", stacked with overflow:hidden on the parent container for rounded corners.
Why: The brief said "HTML using design tokens — feels editorial, not a box-and-arrow diagram." CSS surface tiers do this without any SVG coordinate math. The result is responsive and themeable.
D-18 · Stat figure attribution is "Stanford AI Index, 2024" — verify before launch
Chose: Used "Stanford AI Index, 2024" as the attribution for the 97% figure. Why: This is the most widely cited source for that statistic. Fenja should verify the exact citation and year before the hub is shown to external participants.