# VeriWorkly — Product Reference **Purpose of this file:** a single, exhaustive source of truth for what VeriWorkly is, what it currently does, and where it's going. Everything under "Current Features" below is verified directly against the codebase (routes, controllers, services, middleware, Prisma schema, feature folders, config) as of **2026-08-23**, on branch `master`. Everything under "Vision & Roadmap" is direction, not a shipped guarantee. Where the doc's earlier version turned out to be stale, where marketing copy overpromises what's actually built, or where the code itself is internally inconsistent, that's called out explicitly in [Known Gaps & Inconsistencies](#known-gaps--inconsistencies) rather than papered over. **Status of the public docs:** `docs-platform` (docs.veriworkly.com) was audited page-by-page against the codebase and fully rewritten — its `content/docs` tree is current. `blog-platform` has 21 published posts and its architecture post is accurate. `apps/site`'s machine-readable layer (`llms.txt`, `pricing.md`) has also caught up on the licensing and credit-pack numbers that previously disagreed with the backend. > **Maintenance rule:** this file cannot lie. If you change a quota, price, entitlement, route, default, or gate in code, update the corresponding claim here in the same PR. When a claim here is found to be stale, correct it _and_ record what it used to say — the [Known Gaps](#known-gaps--inconsistencies) list keeps closed items visible rather than deleting them, so this file's own history stays auditable. > > **Last full re-verification:** 2026-08-23. Items marked ✅ RESOLVED in Known Gaps were re-checked against code on that date and confirmed fixed. **A note on pace of change:** VeriWorkly ships fast, and this file goes stale faster than it looks. The 2026-07-29 re-verification found **eight** claims in the 2026-07-24 version that had already become wrong in five days — including the export dispatcher, section reordering, the theme picker, the credit-pack catalog, the ambassador application schema, and the cron job count. Two more (`multiple share links per document`, and the health endpoint's behaviour) had been wrong all along. The practical consequence: **treat any claim here as provisional if it hasn't been re-verified recently, and check the code before repeating a specific number, quota, route, or default externally.** Sections most prone to drift, in order: Ambassador (mid-rollout), Pricing (catalog + UI change independently), Portfolio (`apps/portfolio` is actively being brought to general availability — see §5), and the Known Gaps list itself. All features described below are real, built, and working — some are available to every user, others are being brought to full public availability in stages (Portfolio's publish/checkout step, the Ambassador program's rewards layer). --- ## Table of Contents 1. [Overview](#overview) 2. [Vision & Values](#vision--values) 3. [Core Architecture Concepts](#core-architecture-concepts) 4. [Document Builder (Studio) — Feature Catalog](#document-builder-studio--feature-catalog) 5. [Portfolio Builder — Feature Catalog](#portfolio-builder--feature-catalog) 6. [Affiliate & Ambassador Programs](#affiliate--ambassador-programs) 7. [Marketing Site (site) — Feature Catalog](#marketing-site-site--feature-catalog) 8. [Platform / Backend Capabilities](#platform--backend-capabilities) 9. [Pricing & Monetization](#pricing--monetization) 10. [Known Gaps & Inconsistencies](#known-gaps--inconsistencies) 11. [Vision & Roadmap](#vision--roadmap) 12. [Documentation & Content Debt](#documentation--content-debt) 13. [Appendix: Data Model Reference](#appendix-data-model-reference) 14. [Change log for this file](#change-log-for-this-file) --- ## Overview VeriWorkly is a privacy-first career workspace: a resume and cover letter builder, a public portfolio/website builder, and the AI and career tooling (ATS scoring, AI rewriting, profile import) that connects them — all built around one shared **Master Profile** so a user's career facts live in one place and flow into every document and every portfolio, without login being required to start. **Monorepo layout** (`apps/`): | App | Role | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `site` | Public marketing site (veriworkly.com) — landing, pricing, features, how-it-works, roadmap, FAQ, affiliate/ambassador program pages, security/responsible-disclosure page, public GitHub-activity stats page, public style guide, legal pages, and a machine-readable "AI answer engine" content layer (see §7). | | `studio` | The document builder and authenticated workspace (app.veriworkly.com) — resumes, cover letters, Master Profile, ATS checker, AI tools, sharing, billing, API keys, affiliate/ambassador dashboards, admin. | | `portfolio` | The portfolio/website builder — public template gallery + marketing, authenticated editor/dashboard, and the published portfolio sites themselves (served on `*.veriworkly.com` subdomains and at `portfolio.veriworkly.com/portfolio/{slug}`). | | `server` | Node/Express + TypeScript API backing everything above — auth, documents, billing, AI, ATS, sharing, portfolios, affiliate/ambassador programs, roadmap, admin. PostgreSQL via Prisma, Redis for caching/rate-limiting/counters/locks, plus a set of scheduled background jobs (see §8). | | `docs-platform` | Technical/user documentation site (docs.veriworkly.com), built on Fumadocs. Fully rewritten and verified against code on 2026-07-29 — see [Documentation & Content Debt](#documentation--content-debt). Three trees: `/docs`, `/api-reference` (generated from the OpenAPI spec) and, since 2026-10-19, `/ats-engine` (the open-source engine's developer docs, its reference pages generated from the installed package). Its production build passes again (see [Known Gaps](#known-gaps--inconsistencies) #20). | | `blog-platform` | Official blog (blog.veriworkly.com) with 21 published posts and an accurate multi-app architecture guide. | | `packages/ui` | Shared internal design-system/component library (`@veriworkly/ui`) — primitives (Card, Menu, Badge, Input, Modal, Button, Select, Switch, Tooltip, TextArea, Checkbox, Accordion), layout shells (AppShell, Container, MarketingFooter, the full marketing navbar suite), a ThemeToggle, SocialIcons, and the shared light/dark theme token set (`styles/themes.css`). Consumed by `site`, `docs-platform`, `blog-platform`, and parts of `studio`. | **Tech stack** (versions verified against `package.json` on 2026-10-04): **Next.js 16.3.8** (App Router) + **React 19.3.0** + **Tailwind CSS 4** across all five frontend apps, with **TypeScript 6**; Node.js 22.12+/**Express 4**/TypeScript on the backend (clustered via `throng` in production — see §8); PostgreSQL via **Prisma 7**; Redis for caching, counters, distributed locks, and rate limiting; Better-Auth for authentication (passwordless email OTP plus Google/GitHub/LinkedIn OAuth); Zustand for client state with **localStorage** persistence; `@react-pdf/renderer` and the `docx` package for document export — **both are Studio (client-side) dependencies**, and no export path uses a headless browser (Playwright is only a Studio dev dependency for its browser test suite; Puppeteer is not used); `pdf-parse` and `mammoth` for _server-side_ extraction of uploaded PDFs/DOCX; Cloudflare R2 (S3-compatible) for portfolio image storage; Dodo Payments for billing/checkout; `node-cron` for scheduled jobs. AI features are powered by a mix of **Anthropic Claude and OpenAI GPT models**, dynamically routed per request via an OpenAI-compatible client to balance quality against cost. **Not in the stack, despite occasional assumptions to the contrary:** there is no Radix UI, no MUI, no headless-UI library (`packages/ui` is entirely in-house), no React Hook Form or any other form library (forms use controlled React state plus Zod), no Stripe, and no Playwright/browser-driven E2E suite. --- ## Vision & Values **Product purpose:** VeriWorkly should help users turn verified career facts into clear professional writing and a credible public presence — without obscuring what an AI changed, consuming credits unexpectedly, or locking a user's own data into the service. **Who it's for:** job seekers, independent professionals, developers, designers, and founders who need career documents and a public portfolio, and who expect immediate visual feedback, reliable exports, optional cloud sync, and clear control over their own data. **Core commitments:** - **Local-first, not login-first.** Anyone can open the app and start building a resume or cover letter immediately — no account, no payment wall. Documents are saved in the browser first; logging in adds optional cloud sync across devices. - **Ownership over lock-in.** A user's source data (Master Profile, documents) is theirs; the product avoids the surveillance-heavy, data-hoarding pattern of typical resume SaaS. - **No selling of user data**, and no third-party behavioral tracking (mouse tracking, heatmaps, ad trackers). The product does run its own first-party, aggregate usage metrics (e.g. counts of resumes created, exports, logins) purely to understand product usage — this is internal product telemetry, not user surveillance or data resale, and is worth distinguishing clearly in any public-facing privacy copy. - **Transparent AI.** AI cost (credits) and mode (standard vs. expert) are shown before generation, and AI-generated content requires an explicit "replace" action from the user rather than silently overwriting their work. - **Accessibility target:** WCAG 2.2 Level AA across the product — keyboard access, visible focus states, readable contrast, non-color status cues, reduced-motion support. In practice, Studio's own internal audit (`apps/studio/AUDIT.md`) still flags real gaps against this target — see §4's callouts (missing `aria-expanded`/`aria-controls` on accordions, icon-only social links with no `sr-only` fallback, a login carousel with no reduced-motion handling) — worth treating as an open punch list rather than a completed guarantee. - **Brand personality:** precise, trustworthy, quietly ambitious for the core product; precise, confident, and creatively opinionated for the public-facing Portfolio product specifically. Portfolio's own internal brand brief (`apps/portfolio/PRODUCT.md`) is more explicit than this doc previously captured: the public/marketing surface is allowed to feel bold and aspirational, while the authenticated workspace is deliberately told **not** to "look like a second Studio documents app" — it should feel calm, work-first, and keep expression (the portfolio itself) visually separate from operation (the tools used to build it). --- ## Core Architecture Concepts These ideas cut across every feature below, so they're explained once here. ### Unified Document model Resumes, cover letters, portfolios, and "link-in-bio" pages are all the same underlying `Document` record, distinguished by a `type` field (`RESUME` / `COVER_LETTER` / `PORTFOLIO` / `LINK_IN_BIO`). Each document has JSON content (following a JSON-Resume-like structure), a template ID, a visibility setting (`PRIVATE` / `UNLISTED` / `PUBLIC`), tags, a schema-version field, soft-delete support (`deletedAt`), and an optimistic-concurrency revision number so simultaneous edits across devices don't silently clobber each other. A working `restoreDocument`/`hardDeleteDocument` pair exists at the service layer, but no route currently exposes either — only soft-delete is reachable today (see [Known Gaps](#known-gaps--inconsistencies) #6). ### Master Profile A single, canonical profile per user — every section a resume can have (basics, experience, education, skills, projects, certifications, languages, awards, publications, volunteer work, references, and more) stored as one JSON record, with no other structural fields. - **How it's used:** when a user creates a _new_ resume, cover letter, or portfolio with no content yet, it auto-seeds from the Master Profile. - **The one-way rule:** editing a specific document (a resume, say) never writes back to the Master Profile. Only an explicit action — importing GitHub/LinkedIn data with "replace master" checked, or directly editing the Master Profile page — updates it. This means a user can safely tailor one resume for a specific job without worrying it will change their canonical data or any other document. - The Master Profile is managed inside Studio (`/profile/master`, plus an `/profile/advanced` raw-JSON editor) and is meant to be shared across Studio and Portfolio, though Portfolio's own profile page today is read-only and links out to Studio to actually edit it (see [Portfolio Builder](#portfolio-builder--feature-catalog)). - **Gating:** the read/write endpoints (`GET`/`PUT /profiles/master`) and the auto-seed-on-create logic carry no entitlement check at all — Master Profile access is effectively available to every authenticated user today, regardless of plan. See [Known Gaps](#known-gaps--inconsistencies) #2. ### Local-first storage + cloud sync Every document is written to the browser's localStorage first (so nothing is lost offline or before login), then — if the user is logged in and auto-sync is enabled — pushed to the backend through a background sync engine with retry handling, per-document conflict detection (local vs. cloud), and a per-document opt-out ("keep local only"). Guest (logged-out) users get a persisted local session (a 30-day guest cookie, `veriworkly-guest-mode`, set in the browser with `document.cookie`, so not HttpOnly) so they can keep using the builder without ever creating an account; login is only required for cloud sync, sharing, AI features, ATS, imports, and publishing. The Portfolio app runs a parallel, independently-implemented version of this pattern for the portfolio draft itself: a guest-local-draft vs. cloud-draft merge that compares `updatedAt` timestamps to avoid clobbering newer data on either side. **Typing during a sync is safe.** When a sync finishes, Studio writes only the sync fields (cloud id, revision, status) onto what is stored now, so edits typed while the request was in flight are kept and the document goes back to pending (`recordSuccess` in `document-sync-service.ts`, `patchSync` in `local-storage-service.ts`). ### Entitlements & credits, not simple booleans Access isn't just "free vs. premium." Two systems work together: - **Entitlements** — named capability grants (`ai_credits`, `portfolio_publish`, `custom_subdomain`, `seo_controls`, `analytics`, `watermark_removal`) that can come from an active subscription, a manual admin grant, a promotion, or the system itself. Each has its own start/end window. - **AI credit wallet** — a full ledger (balance, reserved, lifetime credited/debited) with individual expiring credit grants, a two-phase reserve → commit/release flow for AI calls in progress (so a failed generation doesn't burn credits), and a complete transaction history. Under the hood, a debit doesn't just subtract from a single balance: a dedicated allocation table (`CreditUsageAllocation`) records exactly which credit grant(s) funded each transaction on a FIFO-by-expiry basis, so a user with multiple partial grants (e.g. a subscription grant plus a leftover top-up) gets billed from the soonest-to-expire grant first. AI actions have separate "standard" and "expert" cost tiers. This is why free-tier limits aren't a single flat rule across the product — see [Pricing & Monetization](#pricing--monetization) and the reconciliation notes in [Known Gaps](#known-gaps--inconsistencies) for where this gets genuinely inconsistent between the pricing page and the backend logic. --- ## Document Builder (Studio) — Feature Catalog The primary workspace at app.veriworkly.com. No AI/PDF-parsing happens client-side — Studio is the editor and UI; the server does extraction, AI generation, and scoring. ### Resume builder Full section-based editor: Basics, Summary, Experience, Education, Projects, Skills, Links, Languages, Interests, Awards, Certifications, Publications, References, Volunteer, Achievements, plus custom/generic sections. Sections can be shown/hidden individually and reordered in the section settings panel (`SectionVisibilitySettings.tsx`), by dragging a section card or with its "Position" dropdown. A paged, live, pixel-accurate preview updates as you type. Seven resume templates today: **Executive Clarity** (id `executive-clarity`, refined spacing), **Precision ATS** (id `precision-ats`, dense single-column), **Modern Minimal** (id `modern-minimal`, rule-free and whitespace-led), **Timeline Focus** (id `timeline-focus`, fixed date gutter), **Corporate Brief** (id `corporate-brief`, split letterhead), **Bold Impact** (id `bold-impact`, centered high-contrast masthead), and **VeriWorkly Special** (id `veriworkly-special`, high-impact branded layout) — all freely selectable, no template lock for resumes. All seven are built on the shared skin engine in `apps/studio/templates/resume/shared/`, which derives the live preview and the PDF export from one type scale, one spacing model, and one content model, so the two cannot drift apart. **Blank fields are left out of exports.** Placeholder words the editor shows for empty fields (`"Start - End"`, `"Role"`, `"Company"`) never reach a PDF, DOCX, HTML, Markdown or text file, and hidden sections are left out of every format. `tests/contracts/export-hygiene.contract.test.tsx` checks this for every template. ### Cover letter builder A dedicated editor with its own fields/schema, same live-preview and export pipeline as resumes. Five templates: **Professional** (id `professional`), **VeriWorkly Special** (id `veriworkly-special`), **Minimalist** (id `minimalist`), **Executive** (id `executive`), and **ATS Essential** (id `ats-essential`) — also all freely selectable. **Export is type-driven.** `exportDocumentByType` looks up the document type in the document registry (`features/documents/core/registry.tsx`), checks the format is one that type supports, and loads that type's exporter with a dynamic `import()`. A new document type registers its own exporter there. _(Only these two document types — resume and cover letter — exist in code today. Invoices, formal letters, and other document types described in the product vision are not yet built.)_ ### Master Profile Described above — its own dedicated, login-gated editor page (`/profile/master`) plus an `/profile/advanced` raw-JSON editor for deeper/manual detail. ### Import Several distinct ways to get content into the builder, all backend-processed: - **Generic JSON import** — bring in a document exported from elsewhere, via the editor toolbar's Actions menu. - **Markdown import** — a hand-authored markdown resume/cover-letter format, hand-parsed (section headers, date ranges, bold/italic stripping), also via the toolbar Actions menu. - **File extraction** — upload a PDF, DOCX, TXT, MD, or JSON resume (label advertises a 5MB max) and have its text extracted server-side for reuse, used both standalone and inside the ATS checker and the import flows below. The 5MB limit is enforced server-side but **not** checked client-side before upload — a larger file will still attempt to upload and only fail once it reaches the server. - **GitHub import** — a real OAuth-based integration: pulls your connected GitHub profile plus up to 30 repositories, and _deterministically_ (no AI) maps languages to skills and repos to project entries. Free users can only import their own connected GitHub account, once per day. Paid users can import any GitHub username with no per-day quota check in the product-facing sense — but there is a hidden secondary cap of **50 imports/day per paid user**, put in place specifically to protect the single shared GitHub API token's 5,000-requests/hour budget from being exhausted by one heavy user; this cap has no user-facing messaging today. - **LinkedIn import** — not an OAuth/API pull. A user pastes their exported LinkedIn profile text (or uploads a LinkedIn-exported PDF, which gets auto-extracted into the same paste box) and an AI call parses it into structured resume data. Free users get one import per month; paid users get unlimited, with no secondary cap (unlike GitHub, this path isn't bound to a rate-limited third-party API). - Both GitHub and LinkedIn import share a single "Replace my Master Profile with this data" checkbox in the same modal, and always create a new resume document from the result. - **AI resume conversion** — paste or upload an existing resume in any format and have AI restructure it into a proper, editable VeriWorkly document with any template applied. This is an explicit "Pro" feature gated by AI-credit entitlement/quota, and lives inside the ATS workspace ("Pro resume conversion" panel) rather than the main import modal. ### ATS Checker A dedicated workspace (`/ats`, requires login) with two layers of analysis: - **Core scan** — a deterministic, non-AI, free-of-AI-cost rules engine: word count, presence of email/phone/links, Experience/Education/Skills as real headings on their own line, the share of bullets opening with an action verb, the share carrying a metric, length, and format risk. Two format checks read the uploaded PDF's geometry rather than its text — the share of lines split across a column gutter, and ruled table grids traced from the page's drawing operators — and are omitted from the report entirely when a resume arrives as pasted text with no geometry to measure. Produces a readiness score normalised over the applicable rules, and, if a job description is pasted in, a keyword-match score that segments the posting (dropping about-us/benefits/EEO copy), weights requirements above nice-to-haves and skills above ordinary English, treats "Go or Java" as one choice, and credits skills the resume evidences under another name. **Important nuance:** this core scan and the AI-powered analysis below draw from the _same_ scan-quota bucket, not separate meters — running the free core scan and then the AI analysis on the same resume consumes two scans against one shared quota, not one scan against each of two independent limits. - **AI-powered deep analysis** — layers an LLM's explanation, missing-evidence detection, prioritized recommendations, and keyword opportunities on top of the core scan; can fetch a job description directly from a URL instead of requiring paste, with strong server-side SSRF protections (see [Platform capabilities](#platform--backend-capabilities)). - **Quotas:** anonymous visitors get 1 scan per 48 hours; free logged-in users get 2 scans per 24 hours; paid subscribers get 300 scans per billing period (anchored to the subscriber's actual subscription period, not a calendar month). - A previously saved Studio resume can be loaded directly as the scan source, or a resume can be uploaded/pasted fresh. - **The core scan is the open-source `@veriworkly/ats-engine`** (MIT, npm; the server, site and Studio all depend on it). It has its own developer product page at `/ats-engine` on the marketing site and its own docs tree at `docs.veriworkly.com/ats-engine`. The engine is free to self-host, so the quotas above apply to VeriWorkly's hosted checker and API only; there is no separate developer plan for the hosted API. ### AI writing assistance An "Improve with AI" control embedded throughout the editors — select text or a section, choose Standard or Expert mode (different credit costs), optionally give free-text direction, and review the AI's draft before choosing to replace the field or discard it. The credit cost for the selected mode is shown on the mode buttons themselves and again on the Generate button before the user commits to spending credits. Underlying AI actions cover rewriting short text, rewriting/generating whole sections, generating a cover letter, tailoring a resume to a specific job description, and generating portfolio copy (used by the Portfolio product — see below). The model/route chosen per request is dynamically selected across Anthropic Claude and OpenAI GPT models to balance quality against AI cost. ### Sharing Any document can be turned into a public link: - **Password protection** (optional, can be added/removed after the fact; verified server-side with `scrypt` + a timing-safe comparison — not a general account-password scheme, just for share links). - **Expiry control** (a specific date, or no expiry). - **Username-scoped public URLs** (`/share/{username}/{slug}`). A username must be set before sharing is available. - **View analytics** — total view count and last-viewed time per link. **One share link per document**, not many: `ShareLink` carries a `@@unique([userId, documentId])` constraint, so re-sharing updates the existing link rather than minting a second one. _(An earlier version of this doc claimed "multiple simultaneous share links per document are supported" — that is wrong and contradicted by the schema.)_ A link is independently revocable, which takes the public page down immediately. - The public page shows a static snapshot from share time, not a live-linked document, so further private edits don't change what's already been shared until the link is refreshed. This is deliberate: a link already sent to a recruiter can't silently change under them mid-review. - Password verification is rate-limited to **3 attempts / 5 minutes**, and repeat views from one IP within a 30-minute window are deduplicated before the counter increments. ### Export Every document (resume or cover letter) can be exported as **PDF, DOCX, HTML, Markdown, plain text, or JSON**, all from one export menu, available both inside the editor and on the public share page's download menu. ### Workspace / dashboard Document list and grid views, cross-document search, sync-status indicators (local-only / pending / syncing / synced / conflicted) with a details view for resolving conflicts, and a "New Document" flow that includes the import-from-LinkedIn/GitHub entry points described above. Beyond the document list itself: - **Global command palette** — `Cmd/Ctrl+K` opens a fuzzy search across the whole document library (title/subtitle, sorted by recency, top 10 results). - **Theme system** — light / dark / **follow-system**, all three surfaced as explicit options in `/settings` → Appearance (via `next-themes`), plus a toggle in the account menu. _(An earlier version of this doc said there was no UI path back to "follow system" once an explicit theme was picked — a `system` option now exists.)_ - **Sync settings page** (`/settings`): Appearance (theme) and Sync (auto-sync toggle, manual "Sync Now", and the conflict-resolution details modal referenced above), plus a link to `/api-keys`. - **Account deletion** is self-serve on the profile page (`/profile`, "Danger Zone"). It calls `DELETE /users/me`. The dialog says how many documents are on this device and how many were never saved to the cloud, and offers "Download my documents" first. - **PWA support** — the app ships an installable web-app manifest (standalone display mode, maskable icons), not previously documented. - **Destructive actions ask first.** Deleting a document, "Reset to Defaults" and "Empty Fields" each open a confirmation dialog (`ToolbarActionsMenu.tsx`). There is no undo/redo in the resume or cover letter editor. - **Phone numbers are international.** A number starting with `+` is checked with `libphonenumber-js` for any country and stored in E.164 form; older unprefixed numbers are still accepted (`packages/profile-core/src/common/phone.ts`). - **Internal debug tool.** `/pdf-debug/[type]/[templateId]` is a dev-only PDF-rendering debugger. The page returns 404 unless `NODE_ENV` is `development`, and its link shows only in development. ### Credits & billing (in-app) A `/credits` page showing wallet balance, per-action Standard/Expert credit costs, lifetime credited/used totals, expiry of upcoming credit grants, and full transaction history (each entry showing the action, reason, amount, and resulting balance — see the FIFO multi-grant allocation note in [Core Architecture Concepts](#core-architecture-concepts)); a `/billing` page for plan status, entitlements, credit top-up purchase, and managing/canceling a subscription; `/checkout` for the actual purchase flow, which validates the requested product key and billing interval against a whitelist before handing off to Dodo. **Payments are admin-only in production for now.** Anyone else who clicks Buy sees a "Payments open soon" notice, and nothing is charged. ### Developer API keys A full self-serve API key management UI (`/api-keys`, `/api-keys/create`, `/api-keys/[id]`): create a key with a chosen name and scopes, view/rotate/revoke existing keys, and see a one-time reveal of the generated secret. Keys are prefixed `vw_` followed by 64 hex characters. **The backend accepts exactly eight scopes** (`ALLOWED_SCOPES` in `apiKeyService.ts`): `user:read`, `user:write`, `resume:read`, `resume:write`, `roadmap:read`, **`changelog:read`**, `github:read`, and **`ai:write`**. Requesting anything outside that set fails with `400 Unsupported API key scope(s)`. Two precision notes worth carrying into any public copy: - **There is no `roadmap:write` and no `github:write`.** Both of those surfaces are read-only for API keys. (Earlier docs listed both as supported; they never existed.) - **Studio's scope picker offers all eight**, `changelog:read` included (`features/api-keys/components/ApiKeyScopes.ts`). A key created without explicit scopes defaults to `user:read` only. Keys are stored as irreversible HMAC-SHA256 hashes (never plaintext), rate-limited per key (default 20 requests/15-minute window), auto-expiring (365 days by default unless set otherwise), and force-invalidated within 5 minutes of the owning account's subscription lapsing to canceled/inactive (the auth-result cache is explicitly busted the moment the billing webhook processes the cancellation, rather than waiting out its normal 5-minute TTL). ### Affiliate & Ambassador program Full dashboards for both programs live in Studio. See the dedicated [Affiliate & Ambassador Programs](#affiliate--ambassador-programs) section below for complete detail on both, including how thin the Ambassador program's actual backend is relative to its public marketing. ### Admin tools An internal admin area (`/admin/*`, gated by a real role check — `requireAdminUser()` on the frontend shell and `adminAuthMiddleware` authoritatively on every API route) covering: Every admin API lives under a single Express router (`src/routes/admin/`), which applies the admin gate once so any route file added under it is protected by construction. Each domain has a matching controller in `src/controllers/admin/`, service in `src/services/admin/`, and validator in `src/validators/admin/`. Every mutating endpoint requires an audit reason and writes an `AdminAuditEntry`. - **Overview** (`/admin`) — cross-domain summary plus an "action queue" of everything awaiting a human decision (pending ambassador applications, withdrawal requests, unreleased commissions, failed webhooks, suspended portfolios, stuck asset uploads). - **Users** (`/admin/users`) — searchable roster filtered by role, subscription state, and affiliate/ambassador standing; a per-account 360° view (subscriptions, entitlements, credit ledger, documents, portfolio, API keys, sessions, admin history); role changes, session revocation, and confirmed account deletion. The account named by `ADMIN_EMAIL` cannot be demoted or deleted from the dashboard. - **Ambassadors** (`/admin/ambassadors`) — the application review queue with approve/reject and a review note, a per-application detail view, and the roster of everyone currently holding the `AMBASSADOR` role. - **Affiliates** (`/admin/affiliates`) — program summary, per-affiliate detail (referrals, commissions, payouts, top referrer hosts), tier/status moderation, plus dedicated commission, withdrawal, and referral queues. - **Portfolios** (`/admin/portfolios`) — every published subdomain with owner and traffic, a per-portfolio view with a daily view series and top referrers, and moderation (suspend, which clears the Redis copy, the public list, and the portfolio app's ISR cache in one request; or a hard unpublish that frees the subdomain). - **Documents & share links** (`/admin/documents`, `/admin/share-links`) — content moderation that can only reduce reach (an admin can force a document private or unlisted but never publish someone else's private document), soft delete/restore, and share-link revocation. - **Billing** (`/admin/billing`) — subscriptions with a support override for lost webhooks, credit wallets and manual adjustments, entitlement grants/revocations, and the provider webhook log with replay for transiently failed events. - **API keys** (`/admin/api-keys`) — disable, re-quota, or permanently revoke any issued key. Key material is never readable here; only the stored prefix/suffix are returned. - **Audit log** (`/admin/audit`) — filterable, append-only trail of every mutating admin action with actor and reason. - **System** (`/admin/system`) — per-dependency health probes, background job freshness, usage-metric charts, request-error log, a manual GitHub sync, and a cache flush restricted to platform-owned key prefixes (session records are deliberately unreachable, so no cache flush can sign every user out). - **Roadmap CMS** (`/admin/roadmap`) — create/edit/delete public roadmap items with rich detail: problem/solution narrative, timeline, impact metrics, before/after comparisons. Writes are at `POST|PUT|DELETE /api/v1/admin/roadmap`; the public `/roadmap` router serves reads only. - **Changelog CMS** — admin create/update/delete plus a manual GitHub-Releases sync trigger, at `POST|PUT|DELETE /api/v1/admin/changelog` and `POST /api/v1/admin/changelog/sync`. Still API-only — no dedicated Studio screen. The pre-split `/api/v1/admin/monetization` compatibility surface and the singular `/api/v1/admin/ambassador` alias are gone (no client called them); use `/admin/billing`, `/admin/affiliates` and `/admin/ambassadors`. --- ## Portfolio Builder — Feature Catalog > **Current rollout status:** Portfolio is a fully built, working product that's being brought to general availability in stages. The authenticated workspace (dashboard, editor, analytics, settings, profile) is already open to **every logged-in user in production** — anyone can build and preview a full portfolio today. The remaining rollout step is opening up **publishing** and **checkout** to every account: > > - **Publishing** — currently the admin account publishes first in production while the launch path gets validated end-to-end; both the client (Publish button) and the server (`PortfolioController.publish`) reflect this staging step. Draft editing and unpublishing an already-live site are unaffected and open to everyone. > - **Checkout/payments** on the pricing page — same staged rollout, opening alongside publishing. > - **The internal `/og-generator` tool** — an admin-only internal utility, unrelated to the public product rollout. > > All three currently use the same mechanism: a server-only `ADMIN_EMAIL` check (fail-closed — if `ADMIN_EMAIL` is unset, nobody is treated as an admin). This is separate from the feature-flag system described in [Affiliate & Ambassador Programs](#affiliate--ambassador-programs), which governs those programs' own rollout. As the general-availability rollout progresses, re-check this section against the code. ### Public marketing site Landing page, a full template gallery with per-template long-form detail pages (design philosophy, style guide, FAQs, best-fit guidance), a live read-only preview of each template with demo content. **Note:** earlier versions of this preview page blocked right-click, view-source, text selection, devtools shortcuts, and printing to discourage copying a template's design pre-purchase — those protections have since been **deliberately removed** (not merely undocumented) because they were trivially bypassable and actively broke legitimate use (text selection, Ctrl+F, screen readers). Preview pages today behave like normal web pages. Also included: a pricing page, an FAQ page, per-template Open Graph image generation, JSON-LD structured data across the marketing pages, a sitemap that dynamically includes every live published portfolio subdomain, and a Content-Security-Policy header restricting script/resource origins to same-origin plus `*.veriworkly.com`. ### Templates Four templates, sourced from a **private git submodule** (`template-library/`, pinned to a specific upstream commit — adding a template requires registering a loader in `registry.ts`, adding gallery metadata in `templates/catalog/templates.ts`, and advancing the submodule pointer) and rendered server-side: | Template | Personality | Notable design elements | Access | | ----------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | | **Signal** | Engineers/product leads — structured, technical | Dual dark/light theme, scroll-stacking project cards, mouse-glow bento cards, a real local-time display in the hero | Free (default) | | **Atelier** | Designers/creative builders — expressive, editorial | Print/magazine-style asymmetric masonry, serif headlines, flat 0px-radius cards, drifting orbit spheres | Free | | **Nimbus** | Full-stack/frontend devs — atmospheric, tech-editorial | Cursor-follow ring, per-character text-scramble hover, marquee ticker, magnetic CTA buttons — deliberately **no cards, shadows, or blur anywhere** per its own design spec | **Premium** | | **Cipher** | Developers — interactive terminal emulator | Fully interactive draggable terminal window with command autocomplete and a hidden matrix-rain easter egg (plus a `sudo make-coffee` easter egg) | **Premium** | **Correction from an earlier version of this doc:** the "real local-time display" trait belongs to **Signal and Cipher**, not Nimbus — Nimbus's actual component has no clock anywhere, only a copyright-year stamp. This traces back to a mismatch inside the template library's own marketing "strengths" copy for Nimbus, which lists a live-time feature it doesn't have — worth fixing at the source, not just in this doc (see [Known Gaps](#known-gaps--inconsistencies) #16). **Scope note from the submodule's own README:** only VeriWorkly-issued subdomains are supported at launch — custom-domain routing and certificate automation are explicitly out of scope for now. ### Editor A three-pane workspace (section structure / content / live preview via a real `