Stream Facility
Development Log
A running record of what's been built, what's underway, and what's on the horizon. Updated as the platform grows.
Last updated 2026-07-12
74
Features built
0
In progress
2
Planned
Ad System — Tier Gating, Admin Config & Subscription Visibility
Free-tier users see ads in the Lobby and Library; paid tiers see none. Ads are fully configurable from the admin dashboard — paste any provider's embed code and it goes live immediately, or leave blank for house ads promoting the Indie plan. Purchasing a paid subscription hides ads and the active plan is now clearly shown on the user's profile and in the admin users table.
- ·AdZone component: house ad (Indie upgrade CTA) with fallback; script tags injected via DOM creation so third-party scripts execute correctly in both RSC and client components
- ·Lobby: two ad placements (after Authors, between Coming Soon and New Releases); Library refactored to server wrapper + LibraryClient so showAds resolves server-side
- ·Admin → Ads page: paste any provider embed code (AdSense, Carbon, custom); status indicator (house ads vs. custom active); provider quick-reference guide; clear button
- ·Config API patched to merge per top-level key so Tier Config and Ads pages save independently without overwriting each other
- ·Stripe webhook fully wired: checkout.session.completed sets tier on purchase, customer.subscription.deleted reverts to free, invoice.payment_failed marks past_due
- ·Profile page: new Plan & Subscription section — tier badge, benefit summary, Manage Billing button (Stripe portal) for paid users, past-due warning with payment update CTA
- ·Admin Users table: tier cell now shows Stripe/manual source and subscription status inline for all paid-tier users
Security Fix: Forge Access Gate & Admin Tier Guard
Two security issues corrected after the unified profile model eliminated the author role. Every logged-in user was being redirected to /pricing when clicking The Forge because the gate was still checking role === 'author'. Separately, the admin users page could silently downgrade any user's tier to 'free' when an admin changed their role.
- ·Lobby Forge link now gates on isLoggedIn instead of role === 'author' — any authenticated user reaches the dashboard; tier limits apply inside
- ·Admin users page ROLE_DEFAULT_TIER had reader: 'free' — removed; all accounts are role: 'reader' in the unified model so this entry would fire on every role change
- ·Only admin: 'pro' remains in ROLE_DEFAULT_TIER, which is the only role change that correctly implies a tier upgrade
- ·Files: src/app/(outside)/lobby/page.tsx, src/app/(admin)/generalmanagement/users/page.tsx
Unified Author/Reader Profile Model
Eliminated the author/reader registration toggle in favour of a single unified account. Author status is earned (and sticky) the moment a draft is made available or a book is set public. Free-tier users can access the Forge with configurable limits; all tier limits are now admin-editable via the management dashboard.
- ·isAuthor flag earned when draft accessMode→free/paid or book isPublic→true; never revoked
- ·isAuthor carried in JWT and refreshed every 5 minutes from DB
- ·Free tier gets Forge access: 1 world, 1 book, 0 series, 25 codex entries, 0 eras, 1 draft
- ·TierConfig expanded with maxSeries, maxCodexEntries, maxEras, maxDrafts, canAccessForge
- ·All 5 new limits are admin-configurable via /generalmanagement/tiers
- ·InsideNav unified — all users see the same nav including Forge and Studio links
- ·Discovery queries (lobby, authors, search) filter by isAuthor:true instead of role
- ·Registration page simplified to name/email/password — no role selection
- ·Migration endpoint at POST /api/admin/migrations/set-is-author backfills existing authors
- ·Public /readers/[id] profile works for all users; label shows Author or Member dynamically
Lobby: Wing Directory
The lobby page now has a four-panel wing directory between the masthead and the following feed. Each panel is a clickable entryway to one of the platform's four areas — The Hearth (Library), Local & You (Authors), The Stage (Beta), and The Forge (Dashboard/Studio).
- ·gap-px bg-sf-border grid — the border colour is the gap; panels sit flush against each other
- ·Short h-px w-8 rule at the top of each panel encodes wing personality in colour
- ·The Hearth (gold): /library if signed in, /sign-in if not
- ·Local & You (dim gold): /authors — community discovery
- ·The Stage (crimson): /beta — open manuscripts for beta reading
- ·The Forge (subtle or border): /dashboard if author, /pricing if not
- ·sm:grid-cols-2 lg:grid-cols-4 — 2×2 on tablet, 4-wide on desktop
- ·Compass direction labels removed — those were internal book references, not UI copy
Slug URL System
All public and dashboard URLs now use human-readable slugs instead of raw MongoDB ObjectIds. Slugs are generated at creation time and lazily migrated on first visit for existing entities. The server wrapper pattern keeps client components decoupled from slug resolution.
- ·slugify() + resolveEntitySlug() in src/lib/slugUtils.ts handle creation and resolution
- ·Lazy migration: first ObjectId visit generates and saves the slug, then redirects
- ·Server wrapper pattern: thin page.tsx resolves slug → ObjectId; PageClient.tsx receives OID
- ·Dashboard pages updated: books, series, worlds, codex pages, timeline, currently-writing widget, global codex, persona links
- ·MongoDB projection caveat: restricted projections must explicitly include slug: 1
- ·Edit subroutes (/edit) use useParams() and continue to accept ObjectIds — slug support deferred
Email System: Verification, Daily Digest & Author Mailing Lists
The platform now has a complete email system built on Resend. New accounts must verify their email before accessing the dashboard. Authors get a daily digest of unread notifications instead of individual emails. Authors can collect subscribers via an external mailing list link or the built-in subscriber system with their own Resend key.
- ·Email verification: new accounts created with emailVerified=false; 24-hour token sent on registration; middleware blocks /dashboard and /studio until verified
- ·Daily notification digest cron at 08:00 UTC: groups unread notifications per author into one email; digestSentAt field prevents double-sending; per-author opt-out toggle in Profile
- ·Author mailing list — external path: paste a Mailchimp/ConvertKit/etc URL in Profile; 'Join my mailing list' button appears on public profile
- ·Author mailing list — platform path: store own Resend API key (AES-256-GCM encrypted) + verified from-address; native subscribe widget on public profile
- ·Subscriber endpoint rate-limited by IP; unsubscribe via tokenized one-click link appended to every campaign email
- ·Campaign sending batches 50 recipients at a time using author's own Resend key (author owns cost + CAN-SPAM compliance)
- ·NextAuth v5 split-config: auth.config.ts (edge-safe) used by middleware; auth.ts spreads it and adds Node.js-dependent Credentials provider
Security Hardening: Pre-Production Audit
Full security audit completed before moving to a live server. All HIGH and MEDIUM severity issues resolved. Covers authentication, authorization, injection prevention, rate limiting, content security headers, account data isolation, and ESLint security rules.
- ·JWT: 24-hour maxAge; 5-minute periodic DB re-validation; force sign-out on suspension, deletion, or post-change tokens; passwordChangedAt set on every password change
- ·Rate limiting on registration, login, forgot-password, email verification resend, and subscriber signup — all via login_attempts collection with namespaced keys
- ·Security headers: CSP, HSTS 2-year preload, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
- ·Central middleware: auth + admin enforcement for /dashboard, /studio, /generalmanagement, /api/admin/
- ·IDOR fixed on gift code lookup (scoped to buyerAuthorId); IDOR on studio progress route (draftId ownership check)
- ·MongoDB injection prevented: entity collection allowlist in AI chat; status field allowlists on books and series
- ·Content-Disposition RFC 5987 encoding on manuscript filenames; magic bytes validation on DOCX/PDF import
- ·authors/me GET uses explicit field allowlist projection (excludes aiKeys, resendApiKey, stripeIds, suspended)
- ·Account deletion expanded to ~50 PII collections; admin/debug-config route deleted
- ·eslint-plugin-security installed with no-eval, no-implied-eval, no-new-func, @typescript-eslint/no-explicit-any as errors
Image Upload Optimization
Two complementary layers keep uploaded images compact. Client-side canvas resizing fires before the upload even starts. Cloudinary URL transforms ensure optimized delivery to readers regardless of what was stored.
- ·Client-side: landscape capped at 2400 px, portrait at 1200 px, files over 2 MB compressed
- ·Canvas → JPEG 0.88 quality output; GIFs pass through unchanged (preserves animation)
- ·Spinner visible during resize phase, not just during the HTTP upload
- ·Cloudinary delivery transform injected into stored URL: c_limit, w_2000, q_auto, f_auto
- ·f_auto serves WebP or AVIF to browsers that support it
Save Confirmation Flash
All 23 codex entity and content detail pages now show a green 'Saved ✓' flash for 2 seconds after a successful save — eliminating silent saves that looked like nothing happened.
- ·SaveButton component manages three states: Saving… / Saved ✓ / Save Changes
- ·Green background on success state for clear visual confirmation
- ·Button disabled during both saving and the 2-second success window
- ·Applied across all codex entity, book, series, excerpt, and era detail pages
Platform Shop
Three product types: subscription gift codes, digital content, and physical products. Gift codes redeemable at /redeem extend or grant a subscription tier.
- ·Gift certificates generate a one-time code after Stripe checkout
- ·Gifted tier stacks with paid tier; same-tier codes extend expiry
- ·Admin creates products with Stripe one-time price IDs
- ·Order history in Dashboard → Orders
Subscription System (Stripe)
Stripe billing for Indie and Pro tiers. Webhook handles subscription events. Admin can override any user's tier manually with a billing note.
- ·Checkout and customer portal via Stripe-hosted pages
- ·Webhook handles checkout.session.completed, subscription updated/deleted, payment failed
- ·Tier enforced live from DB — no stale JWT values
- ·Manual tier overrides in Admin → Users
Email Infrastructure (Resend)
Transactional email via Resend. Silent no-op when RESEND_API_KEY is absent — safe in CI. Wired for password reset, beta approval, manuscript revision notifications, and deadline warnings.
- ·Forgot password: 64-hex token, 1-hour expiry, self-sealing after use
- ·Beta approval email with deadline date and direct read link
- ·Manuscript revised: all approved readers notified of new version
- ·Deadline warning email alongside in-app notification
In-App Notification System
A complete in-app notification system: bell dropdown in the nav (with count badge and 5-item preview) and a full notifications page. Six event types trigger notifications — follows, friend requests, friend accepts, beta comments, beta replies, and beta applications.
- ·NotificationBell in InsideNav: fetches announcements + 5 most recent notifications on mount; red dot when unread; dropdown closes and marks read on link click
- ·TYPE_ICON map: ◎ beta_comment/reply, ◇ beta_application, ✦ friend_request/accepted, ↑ follow, ♦ gift_received, ✓ purchase_confirmed, ★ gift_redeemed
- ·follow notification fires on follow, resolves profile link from slug + isAuthor
- ·friend_request notification fires on POST /api/friends; friend_accepted fires on accept in PUT /api/friends/[id]
- ·beta_reply fan-out: author reply to a scene triggers insertMany to all readers who commented on that scene
- ·/dashboard/notifications page: full list, auto mark-all-read after 800ms, relative timestamps, unread badge
- ·Notifications card added to Dashboard home Social section (now 4-column grid)
- ·Daily digest cron already integrated — digestSentAt prevents double-sending
Shop — Gift From Your Network
Gift certificate purchases can now target a friend directly from a 'From your network' tab in the recipient selector, without having to search by name or email.
- ·Two-tab switcher: Search (by name/email) and From your network
- ·Friends list loaded lazily from GET /api/friends on first tab switch
- ·Accepted friends appear as a scrollable list; clicking sets recipient and returns to the selected state
This log tracks platform features, not individual content. For questions or feedback, get in touch.