Skip to content
Docs for Flare v3.1.0Latest stable · 0de5c7d2Build details ↗Versions & changes
View docs

Keep the handbook useful ​

Documentation ships with the feature. If a change affects what a person can do, what an integration sends or receives, or how an operator runs Flare, update the corresponding guides in the same commit. This applies to coding agents and human contributors alike. The repository’s AGENTS.md makes this part of the definition of done.

Why the same repository? ​

The implementation, examples, screenshots, and documentation can be reviewed and released together. A separate docs repository would make it easier for a feature to ship without its guide. The docs are still an independent static site: Flare does not need VitePress, Vue, or a documentation server at runtime.

Markdown and theme sources live in docs/site/. Its dependencies and lockfile are separate. Build output and prepared media are ignored by Git and excluded from Flare’s Docker build. The asset script reuses the existing docs/images/ and .github/assets/ libraries, converts referenced images to WebP, and copies only the needed recordings. Do not commit duplicates of generated media.

Run locally ​

Use Node.js 24 (the CI version), then run from the repository root:

sh
npm ci --prefix docs/site
npm run dev --prefix docs/site

The development server prints its address. Content, theme, and local-search changes update as you work. Restart dev after adding a new screenshot reference so the asset preparation step runs again.

To build and inspect the actual static output:

sh
npm run check:coverage --prefix docs/site
npm run build --prefix docs/site
npm run preview --prefix docs/site

The output is docs/site/.vitepress/dist/. build checks links, anchor targets, assets, and OpenAPI references after rendering. A missing guide or image should fail the build instead of becoming a broken page for readers.

Keep the handbook visually aligned with Flare ​

The handbook uses Flare's default light and dark palettes, its bundled Inter typeface, rounded panels, and compact controls. Use the appearance switch in the header to change the handbook's theme. This is the documentation site's preference; it does not change your Flare instance or load an instance's custom appearance pack.

npm run assets generates public/flare-theme.css from LIGHT_PALETTE and DARK_PALETTE in lib/customization/defaults.ts and prepares the existing fonts from public/fonts/inter/. Do not maintain a second palette or commit the generated assets. If the palette data stops being literal colors, asset preparation fails so the reader can be updated instead of silently shipping stale defaults. Docs-only surface, spacing, and VitePress mappings live in .vitepress/theme/style.css.

The release builder applies the current shared stylesheet, layout, fonts, and palettes to stable, archived, and rolling pages. Each release retains its original guide text, screenshots, and interactive content. The public root is Flare's product landing page, built from the publishing checkout. Its Read the docs and Get Flare links lead into the stable handbook. Archived homepages keep their original content. Source and website-tooling revisions remain separate in Build details. When changing this presentation, check both themes, mobile navigation, search, tables, code blocks, screenshot dialogs, interactive labs, and the release browser, including a non-root base path.

Product screenshots are prepared separately in public/landing/ from the existing demonstration captures. They show the current product presentation; they are not substituted into historical guides. The release builder temporarily installs the product homepage only for the public root, then restores the original page, component, and image bytes before rendering its archive. The homepage keeps the version/source stamp below its product content and above the creator credit.

The hero's interactive showcase is a local illustration using sample files, with no audio or connection to an instance. Keep its demo label, manual chapter controls, pause behavior, and reduced-motion support. Actual app recordings remain separate in Interactive demos. The release overlay includes the showcase component and its styles; archived homepages keep their original content.

This preview describes the checked-out source, including unreleased changes. The public site uses the separate release build below: the product homepage links to stable documentation, and readers must explicitly open the rolling preview for development prerelease instructions.

Build the release site ​

From a checkout with the complete Git history and release tags:

sh
git fetch origin main 'refs/tags/v*:refs/tags/v*'
npm ci --prefix docs/site
DOCS_BASE=/Flare/ npm run build:releases --prefix docs/site
DOCS_BASE=/Flare/ npm run test:releases --prefix docs/site

The release builder reads GitHub's published release metadata and resolves each stable release tag to its source commit. The latest stable release supplies the default guides, with the current project landing page at the public root. Every published stable release also has an archive at /versions/TAG/ below DOCS_BASE, including its original homepage. A release without the handbook uses the README and any earlier guides from its own tag. The builder does not fill missing historical guides with current instructions.

The same build adds /rolling/ from the immutable commit recorded in the published rolling release's flare-commit-sha marker. The application uses this marker for rolling update checks too. The mutable local rolling tag and the publishing checkout are not substitutes: a local tag can be stale, and main can be ahead of published Docker images. The fetch command retrieves version tags without attempting to overwrite an existing local rolling tag. The builder fetches the published rolling commit if it is missing locally. Drafts and other prereleases are excluded; rolling is separate from the stable archive list and comparisons.

The output is docs/site/.vitepress/releases/. build:releases validates the assembled site; test:releases serves that output and checks stable content, archive navigation, dated releases, comparisons, and explicit rolling access in a browser. Install Chromium as described in Browser checks before the first test run. Keep this generated output, intermediate checkouts, and downloaded release metadata out of Git.

Run one release build at a time. The builder copies the verified site into a sibling staging directory before replacing the previous output, then keeps a backup until the replacement succeeds. A failed copy leaves the old site intact; a failed replacement rolls back, retaining the backup if rollback cannot finish. The output path can briefly be absent between the two directory renames. If a build is interrupted there, the next build:releases run restores the backup before starting its build; after a completed replacement, it removes any leftover backup. The staging and backup directories are generated files and stay out of Git.

The build needs GitHub API access. In CI, GH_TOKEN receives the read-only repository token. Locally, GH_TOKEN or GITHUB_TOKEN can supply authentication, or the builder uses your authenticated GitHub CLI. Public release metadata also works without authentication within GitHub's unauthenticated rate limit. Keep tokens in your environment; never place one in a command committed to this repository. If fetching metadata or a source commit fails, resolve the reported access or Git error and rebuild. Do not substitute main for a missing stable release or rolling commit, or reuse a partial output directory as a deployment.

What a complete update includes ​

If you change…Update and verify…
A user workflowSteps, labels, expected result, privacy/limits, troubleshooting, and current screenshots
An admin controlWho can change it, default, effect on existing users, related user-facing behavior, and recovery
Hosting or persistenceConfiguration reference, deployment commands, backup/restore, upgrade and migration steps
An API routeEndpoint inventory, actual authentication, request and response schema, statuses, units, examples, OpenAPI where token-supported
A webhookEvent schema, signature verification, retry behavior, operating limits, and working receiver example
A capability or navigationFeature explorer, sidebar, cross-links, tour, and examples that mention it

Trace claims to the source. A route existing under /api/ does not mean a named token can call it. A “private” checkbox does not mean administrators cannot read the data. An account export is not a server backup. Explain these boundaries directly.

Do not add filler to satisfy a gate. For an internal change with no user-visible effect, record why the existing guidance remains correct in the PR and review the relevant pages. Coverage checks are structural aids; they cannot establish that the prose is complete or true.

Screenshots and recordings ​

Use a local instance and demonstration data. Never photograph a real token, secret, private file, or customer account. Capture the real interface; do not reconstruct it in an image generator. Inspect the image before adding it, especially after feature removals or renamed controls.

Use the globally registered component:

vue
<Screenshot
  src="/screenshots/handbook/upload-profiles.webp"
  alt="Flare’s upload profile editor showing saved sharing defaults"
  caption="Choose which options a profile overrides; leave others inherited."
/>

/screenshots/ maps to source images under docs/images/; /evidence/ maps to .github/assets/. Use a literal source path so asset preparation can find it. PNG and JPG source paths automatically become generated WebP URLs. New source images should normally already be compact WebP files. Keep one canonical source and let the build prepare it.

Recordings need a useful written transcript, manual playback, and preload="none". Respect reduced-motion preferences. Clearly distinguish recorded real app behavior from the local educational simulations in the interactive labs. Do not add an unlabeled fake server or a demo that secretly calls a reader’s instance.

Project credits and the author button ​

The handbook footer credits Flare’s creator, FlintSH, and links to fl1nt.dev. Keep this credit on the homepage and every guide, alongside the MIT license and xNefas’s icon credit.

ProjectCredit.vue uses the official 88 × 31 button from https://fl1nt.dev/images/mybutton.gif. This follows the website’s Hotlink my Button! → Copy Code snippet. Keep its original dimensions, colors, and pixel art; do not stretch, recolor, or redraw it. The image is hotlinked, so no duplicate brand asset is added to the repository. The visible author name and website remain a working link if the image cannot load.

Browser checks ​

From docs/site/:

sh
npx playwright install chromium
npm test

Tests start a fresh production preview on port 4175 and cover local search, feature filtering, screenshot keyboard dismissal, upload-option precedence, additive role permissions and token scope intersection, safe API code generation, version/commit provenance, desktop/mobile overflow, failed asset requests, and automated WCAG checks in light and dark themes. Stop any previous preview on that port before running them. Reusing a preview after rebuilding can serve stale asset paths. Layout and WCAG checks visit every handbook page; font and palette checks verify the generated Flare defaults. The Git policy fixture also proves that a later documentation commit cannot cover an earlier undocumented feature commit. Linux machines may need Playwright's system dependencies; CI uses npx playwright install --with-deps chromium.

The separate npm run test:releases suite verifies the assembled stable site, release archives, and rolling preview after npm run build:releases. Run both suites when changing version navigation, comparisons, or publishing. Source-preview tests cannot establish that deployment selected the correct stable and rolling revisions.

When changing links or theme components, also test a subpath build:

sh
DOCS_BASE=/flare/ npm run build
DOCS_BASE=/flare/ npm test

DOCS_BASE must begin and end with /. Afterward rebuild without that variable if you want a root deployment. For Vue components, use withBase() for local links and assets. Regular Markdown links are handled by VitePress.

Dependency maintenance ​

Keep Flare's pnpm-lock.yaml and the handbook's package-lock.json separate. Use Node.js 24 and the pnpm version in the root package.json; install with pnpm install --frozen-lockfile and npm ci --prefix docs/site. Check both dependency trees with pnpm audit and npm audit --prefix docs/site before a release. A clean audit describes the advisories known at that time, not a guarantee against future findings.

The application checks that CI runs are pnpm exec tsc --noEmit, pnpm test, pnpm lint, and pnpm format:check. pnpm lint runs ESLint 9 directly with eslint.config.mjs over app, components, lib, hooks, proxy.ts, and instrumentation.ts. It replaced next lint, which Next.js 16 removed. scripts, types, the test files, and the handbook are not linted by it. The pre-commit hook runs the same command after formatting the staged files with Prettier. It commits only what you staged: unstaged and untracked files stay out of the commit, so run pnpm format:check before pushing if you edited files you didn't stage.

The Docker Test workflow builds the image for linux/amd64 and linux/arm64, starts it next to an empty PostgreSQL 16 database, and passes only when the image's own health check reports healthy. The container applies the migrations itself during that start, as it does for operators. If the container stops, its health check reports unhealthy, or it is still starting after 3 minutes, the job prints the health-check results and the container log and fails.

The gallery file-type picker has component regression tests using React Testing Library and jsdom. Run pnpm exec vitest run __tests__/components/file-filters.test.ts to check MIME and readable-label search, selections surviving a nonmatching query, clearing the query on close, and category order. These tests render the actual picker and popover; jsdom does not validate layout, so UI PRs still need reviewed captures from the real application.

The 2.1 dependency refresh keeps the existing application framework versions compatible. The root overrides update Prisma's configuration merger to address vulnerable transitive packages. The handbook separately overrides Vite to its patched 6.4 line while retaining stable VitePress.

The root override keeps Vitest's Vite on a patched release, because dependency updates can leave it at a locked vulnerable version. Tailwind CSS 3 requires postcss-selector-parser 6.x. Keep it on 6.1.5 or later, the maintainers' backport of the 7.1.6 parser fix, rather than forcing 7.x onto Tailwind 3. Until advisory GHSA-rj75-hqrm-r3gf lists 6.1.5 as patched, pnpm audit and Dependabot still report it.

Recheck these overrides against upstream releases before removing them, and run application tests, database migrations, production builds, and local browser checks after changes.

Flare validates with Zod 4, which differs from Zod 3 in ways the type checker does not always catch. .default() returns its value without parsing it, so use .prefault() when an object default should fill its nested defaults. Defaults also apply inside optional fields, so build update schemas from fields without create defaults; otherwise .partial() resets every omitted field. API timestamps that accept offsets use isoDateTimeWithOffset from lib/utils/iso-datetime.ts, which keeps Zod 3's forms (+0200 offsets and optional seconds) for existing clients. Messages without custom text use Zod 4's default wording. Form resolvers need @hookform/resolvers 5.1 or later.

Flare runs on Next.js 16 and React 19. Both next dev and next build use Turbopack, and Flare has no webpack configuration. Code that runs before routes lives in proxy.ts, Next.js 16's name for middleware.ts. It runs in the Node.js runtime, and its matcher skips /api/ so request bodies reach their routes whole. The React Compiler is turned on with the top-level reactCompiler option. eslint-config-next 16 adds the compiler's lint rules from eslint-plugin-react-hooks 7. pnpm lint enforces immutability, purity, and static-components: a component must not change its props or state in place, read the clock or a random value while rendering, or declare another component inside itself. eslint.config.mjs still turns off the three that code written before them breaks: incompatible-library, refs, and set-state-in-effect. Turn a rule back on in the change that fixes its findings.

Build tracing copies every file a server route might read into .next/standalone. A path built from process.cwd() makes it copy the folder that path names, or the whole checkout when the rest of the path is only known at runtime. The local storage provider builds its paths with workingPath, whose /* turbopackIgnore: true */ comment prevents the whole-checkout copy, and outputFileTracingExcludes in next.config.ts keeps uploads/ and tmp/ out of route traces. Neither applies to the trace for instrumentation.ts, so parts of an unfinished local chunked upload in tmp/local-multipart can still be copied there, as in earlier builds. After changing storage paths, check that pnpm build prints no "Dynamic filesystem access" warnings.

next dev keeps its output in .next/dev and runs one development server per checkout. A second next dev in the same checkout prints the address of the one already running instead of starting. Use a separate worktree for each server you need at once. When next dev detects a coding agent, it also keeps a Next.js block at the end of AGENTS.md that points to the documentation bundled in node_modules/next/dist/docs/. Commit changes to that block with the dependency update that caused them. Setting agentRules: false in next.config.ts turns it off.

Workflow actions ​

Every uses: reference in .github/workflows/ names a full commit SHA, followed by a comment with the release it came from, such as actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0. A tag like @v4 can be moved to different code at any time, and the release workflow runs with registry and release credentials. Dependabot doesn't raise security alerts for actions pinned this way, so .github/dependabot.yml asks it for monthly github-actions updates, grouped into one pull request that bumps the SHA and its comment together. Review those like any dependency update: read the action's release notes, especially for a new major version, and check that the workflows still pass.

To add or change an action by hand, resolve the release tag to its commit and pin that, keeping the version comment:

sh
gh api repos/actions/checkout/git/ref/tags/v4.4.0 --jq '.object'

The result shows the target's sha and type. Pin a commit SHA directly. If type is tag, the tag is annotated: resolve once more with gh api repos/<owner>/<repo>/git/tags/<sha> --jq '.object.sha'.

Database permission and avatar regression tests ​

The role/cleanup and avatar race suites require two separate disposable PostgreSQL databases. Create them on a loopback server with names beginning flare_roles_test_ and flare_avatar_test_. Use a local test database role allowed to apply migrations and manage fixture tables. Both suites clear fixture tables in their dedicated databases. Do not use an application or browser-demo database for these tests.

From the repository root, set connection URLs appropriate to that disposable server. These examples assume the local test role can connect without a password; supply your test server's authentication and port when needed:

sh
export FLARE_ROLES_DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_roles_test_local?schema=public'
export FLARE_AVATAR_DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_avatar_test_local?schema=public'
DATABASE_URL="$FLARE_ROLES_DATABASE_URL" pnpm exec prisma migrate deploy
DATABASE_URL="$FLARE_AVATAR_DATABASE_URL" pnpm exec prisma migrate deploy
pnpm exec vitest run \
  __tests__/permissions/database.test.ts \
  __tests__/storage/avatar-database.test.ts

Apply migrations to both databases before running the suites. Missing either environment variable skips that suite; check the test output rather than treating a skipped run as coverage. CI supplies both URLs. The database checks exercise role authority, recovery safeguards, bulk account cleanup, original storage targets, upload/deletion races, and durable avatar intents. They use real PostgreSQL and local files, with controlled S3 SDK responses; they do not substitute for testing a real S3-compatible provider.

The browser/API role recipes cover the rendered controls and real session behavior separately.

Continuous library browser checks and demos ​

The library guide and recorded walkthrough use a disposable account with 12,000 files dated from July 2018 to October 2026. The uploaded landscape illustrations and dates are demonstration fixtures. Captures render the actual application and use its real database and file requests.

Create a disposable local PostgreSQL database named exactly flare_timeline_test_local. The seed accepts only localhost or 127.0.0.1 with the public schema and refuses a database containing accounts other than its two fixtures. It resets those fixture accounts and writes their local file objects beneath uploads/timeline-demo/; do not point it at an existing application installation. Install the root dependencies and the separate handbook dependencies first; the scripts use the handbook's Playwright and Sharp packages. Install Chromium as described in Browser checks before the first run.

From the repository root, use your disposable database role and authentication:

sh
export DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_timeline_test_local'
export NEXTAUTH_URL='http://localhost:3064'
export NEXTAUTH_SECRET='public-disposable-timeline-demo-secret-2026-only'
pnpm exec prisma migrate deploy
node scripts/migrate-config.js
node scripts/timeline/seed.cjs
pnpm exec next dev --hostname 127.0.0.1 --port 3064

In another terminal, run the real browser checks:

sh
export FLARE_TIMELINE_TEST_ORIGIN='http://localhost:3064'
node scripts/timeline/verify-ui.cjs

The demonstration account is timeline-demo-alex@example.test with the deliberately public password Timeline-demo-only-2026!. The second account supplies an ownership-isolation check. The checks use UTC so dates are reproducible; the shipped library uses each viewer's browser time zone. This is local browser coverage, not a benchmark of production storage.

The browser checks cover direct date jumps, bounded mounted cards and file requests, one shared height for cards and loading placeholders whether or not files have tags or a folder, no repeated folder label inside that folder, selection and archive inputs across scrolling, fresh bulk-tag membership after a real external API change, search and folder totals, date grouping, alternate sorts, image navigation, old page links, and position preservation when using browser Back or resizing between desktop and mobile. One retry check simulates a network failure by aborting a request; the retry fetches real files from the local server. The recordings omit that injected failure and the separate archive-input and current-tag membership regressions. Set FLARE_TIMELINE_RESULTS to a temporary JSON path to save the measured counts and check results alongside the console output.

At desktop and mobile widths, the checks also open Create archive after selected cards have scrolled out of the rendered window, verify the selected filenames and private defaults, and cancel without creating an archive. Closing that dialog clears the selection. With screenshot output enabled, the desktop check captures library-archive.webp with the selected-file list expanded. These unrecorded checks complement the separate archive suite’s real creation and extraction operations below.

Separate unrecorded checks create and revoke disposable named tokens, verify selected-ID listing and timeline counts stay within the token owner's account, reject an upload-only token on those reads, and run examples/integrations.mjs files against the real local server. They verify request-level audit attribution to the owner and token ID, including denied reads, without retaining token secrets in audit details or captures.

The retained-selection tagging regression has a separate check against the same disposable server:

sh
export FLARE_TIMELINE_TEST_ORIGIN='http://localhost:3064'
node scripts/timeline/verify-tag-refresh.cjs

It selects a tagged file, removes the tag through another authenticated client, refreshes, and verifies that Edit tags reads current membership and sends an addition when the unchecked tag is clicked. It also checks selected files that have scrolled off screen, mixed membership, recovery from a failed membership request, and cancellation when a loading dialog closes. The failed and delayed requests are simulations; successful reads and tag changes use the real local application. The check restores its fixture tag assignments afterward.

Set FLARE_TIMELINE_SCREENSHOTS to a temporary directory to capture library-tags.webp during the successful membership read, before the simulated request failure. Inspect it before replacing docs/images/timeline/library-tags.webp. This separate tagging image supplements the seven library screenshots captured by verify-ui.cjs.

Thumbnail browser regression ​

With the same disposable timeline database and server running, check thumbnail delivery in the library and another account's View content dialog:

sh
export FLARE_TIMELINE_TEST_ORIGIN='http://localhost:3064'
node scripts/timeline/verify-thumbnails.cjs

Keep DATABASE_URL set to the disposable flare_timeline_test_local database. The check refuses other database names and accounts. It uploads four temporary images through the real API, verifies the original thumbnail bytes and anonymous private/password access boundaries with and without a width, and checks public, private, password-protected, and animated GIF previews at desktop and mobile widths. Every preview must ask the thumbnail route for a width and none may go through /_next/image: PNG previews come back as smaller WebP copies under the file's own cache policy, and the GIF comes back whole so it stays animated. The second seeded administrator supplies the moderation session. Requests and uploads are real; the check uses no network stubs. It deletes its uploads afterward.

Cleanup attempts every tracked upload even when a deletion fails, then closes Chromium. Verification, deletion, and browser-shutdown failures are reported together, and the command exits unsuccessfully. If a deletion failed, restore the local server/session and remove the listed Thumbnail check uploads from Alex's disposable library before retrying; the next run refuses stale fixtures. Cleanup failure tests use simulated responses and rejected requests without making real deletions.

Set FLARE_TIMELINE_SCREENSHOTS to a temporary directory to capture thumbnail-library-desktop.webp, thumbnail-library-mobile.webp, thumbnail-moderation-desktop.webp, and thumbnail-moderation-mobile.webp. Inspect every capture before updating handbook evidence. These checks verify the rendered application; they do not establish a pixel-identical comparison with a baseline.

The shared storage contract tests also delay attaching a reader by a full event-loop turn, for complete and ranged reads. They catch S3 streams that lose their bytes before response setup finishes. The default S3 contract uses an in-memory SDK backend; the optional TEST_S3_* run in __tests__/storage/s3.test.ts exercises a separately configured S3-compatible endpoint. Local browser checks against filesystem storage alone do not establish S3 service compatibility.

__tests__/files/thumbnail-stream.test.ts additionally serves a controlled S3 GET response on a loopback HTTP socket and exercises the actual AWS SDK response stream and audited thumbnail route. The fixture simulates the S3 service response; it does not call a hosted bucket. Both delayed reads and route responses must preserve the exact PNG bytes, including when audit persistence yields before the response is returned.

Account changes in an open library ​

With the same disposable timeline database and server running, check an account change while the original library tab stays open:

sh
export FLARE_TIMELINE_TEST_ORIGIN='http://localhost:3064'
node scripts/timeline/verify-account-switch.cjs

Keep DATABASE_URL set to the same disposable database. The check accepts only the two seed accounts, adds 96 Jamie demonstration file records using existing fixture image bytes, and removes those added records afterward. It retains Alex's selection and an open Create archive dialog, signs in as Jamie through a real form in a second tab, and reloads only that second tab to trigger normal session synchronization. The original document stays open. It verifies that the old files and dialog disappear, Jamie's library loads, and date navigation cannot bring Alex's cards back. It also checks a replacement session for the same account and sign-out from the other tab.

Separate scenarios simulate delayed network delivery by holding completed, real timeline or file-list responses until after the account changes. They check that an old response cannot repopulate the new library; they do not fabricate a successful API response. These checks are separate from the ordinary scrolling recordings.

Set FLARE_TIMELINE_SCREENSHOTS to a temporary directory to capture library-account-switch.webp after the new account loads and passes date-navigation checks. Inspect it before replacing docs/images/timeline/library-account-switch.webp; the image should contain only the current demonstration account's files, with no old selection or dialog.

Set FLARE_TIMELINE_SCHEMA_EVIDENCE to a temporary JSON path to capture actual dated and undated timeline responses for two known fixture IDs. scripts/checks.test.mjs checks the published OpenAPI boundary types against the captured examples in scripts/fixtures/timeline-responses.json, including JSON null for non-date sorts. This is a focused contract regression, not validation of every OpenAPI operation.

Timeline API and capture output ​

The timeline API regression suite needs a separate disposable database, because it clears users and events between cases. For example:

sh
export FLARE_TIMELINE_DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_timeline_test_api'
DATABASE_URL="$FLARE_TIMELINE_DATABASE_URL" pnpm exec prisma migrate deploy
pnpm exec vitest run __tests__/files/timeline-database.test.ts

Create that database first and keep it separate from flare_timeline_test_local. The API suite accepts only PostgreSQL on localhost or 127.0.0.1, with the exact database name flare_timeline_test_api or flare_timeline_test_ci. Omit the query string or use a single schema=public; other query parameters and URL fragments are rejected. Omitting FLARE_TIMELINE_DATABASE_URL skips the database suite; a skipped run is not database coverage. The code-quality CI workflow supplies and migrates its separate flare_timeline_test_ci database before running the suite.

To refresh the screenshots and silent recordings, set FLARE_TIMELINE_SCREENSHOTS to a temporary screenshot directory and FLARE_TIMELINE_VIDEOS to a separate temporary recording directory before running the browser script. Recording requires ffmpeg on your PATH with its libx264 H.264 encoder; ordinary browser checks and screenshots do not require it. The script converts the browser recordings to MP4 for playback in the handbook and PR links. Inspect every capture before replacing the nine canonical WebP sources in docs/images/timeline/ and timeline-scroll.mp4 / timeline-mobile.mp4 in .github/assets/timeline/. The main suite captures seven screenshots; the tag-refresh and account-switch checks each capture one more. Keep one source for each asset; the handbook build prepares its own copies. Update the written transcripts in demos if the recorded actions change. The mobile recording uses Chromium at a narrow viewport, not a physical phone. Keep temporary output, failed recordings, and fixture uploads out of Git.

Run these tools from a source checkout. The timeline fixture/capture scripts, their evidence directories, and the handbook tooling are excluded from Flare's Docker build context and application image.

Archive browser checks and demos ​

The archive workspace uses real uploads, sessions, storage, and database publication. Its local browser script creates ZIP, TAR.GZ, and GZIP fixtures, exercises owner-library browsing and extraction, packages selected files, and checks both private defaults and an explicitly selected public upload profile. It also tests anonymous share-page browsing, file-password protection with the unlock cookie, and individual entry downloads without offering extraction to recipients. A cut-off ZIP and a ZIP with one damaged member check the archive-level and per-entry errors.

Use a disposable local PostgreSQL database named exactly archive_demo. scripts/archives/seed.cjs accepts only localhost or 127.0.0.1 and either no query parameters or a single schema=public. It resets the two public fixture accounts, uses local storage, and disables OCR and OIDC in that disposable instance. Do not point it at an existing installation. Create the database with your local PostgreSQL tools, then run from the repository root:

sh
export DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/archive_demo'
export NEXTAUTH_URL='http://localhost:3062'
export NEXTAUTH_SECRET="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))")"
pnpm exec prisma migrate deploy
pnpm exec next dev --hostname 127.0.0.1 --port 3062

Match the role, port, and database authentication to your disposable PostgreSQL server. In another terminal with the same DATABASE_URL, open http://localhost:3062/auth/login once to initialize configuration, then run:

sh
node scripts/archives/seed.cjs
node scripts/archives/verify-ui.cjs

The public fixtures are archive-demo-alex@example.test and archive-demo-jamie@example.test, both with password Archive-demo-only-2026!. The seed also raises the disposable instance's upload limit to 4 GB for the optional large-archive checks. Reseed before repeating the browser suite. It verifies nested extraction, empty directories, exact downloaded content, unchanged originals, default private output, explicit profile visibility/tags/expiration, TAR.GZ creation, and ownership/session/origin failures. Shared reads separately check anonymous access, protected/private boundaries, body-only passwords on the archive routes, and exact entry downloads. It checks library dialogs and shared browsing at 390px wide and displays a real malformed-archive error. No remote storage provider is involved.

Set FLARE_ARCHIVE_SCREENSHOTS and FLARE_ARCHIVE_VIDEOS to separate temporary output directories to capture evidence. The primary recordings are archive-browse-extract.webm, archive-create.webm, and archive-share-browse.webm; inspect generated screenshots and recordings before replacing sources in docs/images/archives/ and .github/assets/archives/. The shared recording uses an anonymous, unprotected public archive. Protected-file checks produce separate stills; do not capture password values or credential-bearing URLs. Preserve written transcripts in Demos, and keep failed or incidental recordings out of Git. FLARE_ARCHIVE_TEST_ORIGIN can override the default http://localhost:3062, but must remain a disposable HTTP localhost origin matching the running app's NEXTAUTH_URL.

To repeat only the shared-page checks, reseed first, then use the optional mode. It still uploads its own fresh fixtures:

sh
node scripts/archives/seed.cjs
FLARE_ARCHIVE_SHARE_ONLY=true node scripts/archives/verify-ui.cjs

Set FLARE_ARCHIVE_LARGE=true to add the large-archive checks. scripts/archives/large-fixtures.cjs generates, in a temporary directory, a ZIP of about 906 MiB with 20,025 files (20 handbook screenshots, an existing Flare recording, a generated tone and log, 20,000 small files, and 900 MiB of random data) and a TAR.GZ of about 337 MiB. The script uploads both through the chunked upload API in 5 MiB parts, then checks the listing time, the extraction explanation, video seeking with 206 responses, audio, Load more, the image viewer, path search, TAR.GZ reading progress, and a fast preview of the TAR.GZ's last entry. Allow roughly 4 GB of free disk space for the fixtures, the uploaded copies, and multipart staging; the temporary fixtures are removed afterward. The recordings archive-large-preview.webm and archive-large-tgz.webm and the large-*, video-preview, text-preview-more, image-gallery, and tgz-* stills come from this mode.

The archive reader has its own suite, which needs no database or browser. It builds ZIP (including ZIP64, prepended data, CP437 and Unicode names, and problem entries), TAR, TAR.GZ, and GZIP fixtures and checks listings, ranged reads, CRC verification, checkpoint seeking, resume points in multi-member GZIP files, the 1 GiB decompression limit, truncation, the extraction assessment, unlock cookies, and the listing cache. Regression tests also cover hostile PAX headers against tar-stream's parsing, cancelling a stream during a long seek, yielding to timers during large listings, the encoded-listing cache, remembered failures, and the per-requester listing limit:

sh
pnpm exec vitest run __tests__/archives/reader.test.ts

The separate archive database suite requires its own disposable database; do not reuse the browser fixture database:

sh
export FLARE_ARCHIVE_DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_archive_test_local'
DATABASE_URL="$FLARE_ARCHIVE_DATABASE_URL" pnpm exec prisma migrate deploy
pnpm exec vitest run __tests__/archives/database.test.ts

The suite accepts only local PostgreSQL databases named exactly flare_archive_test_local or flare_archive_test_ci, with the public schema. Without FLARE_ARCHIVE_DATABASE_URL it is skipped. It tests atomic publication and rollback, source ownership, quota, upload profiles and stale revisions, storage provenance, and safe filenames against real PostgreSQL, an in-memory storage provider, and real temporary-file codec operations. Shared archive tests also check current visibility/password access, revalidation after reading an archive, separate listing and entry rate budgets, and shared reads joining one listing. The streamed-preview tests cover byte ranges, inline media detection, damaged members, the extraction explanation, 202 progress polling with a cursor, owner role, session, and source changes during progress polls, a progress poll answered while a role change holds its lock, and unlock cookies. The browser demos separately exercise the actual local storage provider. These checks complement the codec and provider tests; they do not establish real S3 compatibility.

Shared-request admission has separate handler and body-guard regression suites:

sh
pnpm exec vitest run \
  __tests__/archives/handler-availability.test.ts \
  __tests__/archives/shared-body.test.ts

They exercise the route handlers and body guards with streamed request bodies and controlled session/database/archive-service responses, without requiring PostgreSQL. They check that unfinished or rejected shared requests cannot occupy archive-processing slots needed by owner operations, alongside the body deadline, pending-read limit, cancellation, and capacity reuse. These request-admission checks complement the real storage/browser evidence above. Admission ordering changes errors and resource allocation without changing rendered controls or successful workflow steps, so the existing screenshots and recordings remain representative.

Security browser checks and demos ​

The sign-in security guide includes real application captures for authenticator setup, both recovery methods, passkeys, and the optional passkey requirement. The passkey recordings use Chromium's virtual authenticator: the application and server perform real WebAuthn ceremonies, while the virtual device stands in for a physical authenticator. They do not show or test a native biometric prompt or a live external SSO provider.

Use a disposable local PostgreSQL database named exactly flare_auth_demo. The seed script accepts only localhost or 127.0.0.1 and rejects other database names, including flare_auth_demo_backup. Omit the schema URL parameter or set it once to public; other schemas, duplicate schema parameters, and all other URL query parameters are rejected. It resets its two public fixture accounts and security rate counters. Never use an existing application database. With the dependencies installed, create the database using your local PostgreSQL tools, then start the app from the repository root:

sh
export DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_auth_demo'
export NEXTAUTH_URL='http://localhost:3061'
export NEXTAUTH_SECRET='public-disposable-security-demo-secret-2026-only'
pnpm exec prisma migrate deploy
pnpm exec next dev --hostname 127.0.0.1 --port 3061

The database role, port, and authentication must match your disposable server. In another terminal with the same DATABASE_URL, initialize the app configuration by opening http://localhost:3061/auth/login, then run:

sh
node scripts/security/seed.cjs
node scripts/security/verify-ui.cjs

The fixture emails are security-demo-alex@example.test and security-demo-jamie@example.test; both use the deliberately public password Security-demo-only-2026!. The browser checks use real requests to enroll an authenticator, require a second factor at login, consume and reject reused authenticator recovery codes, replace codes, disable 2FA, register and use a passkey, rename/remove it, reject assertion replay and removed credentials, reject bearer/origin misuse, and invalidate old sessions. The required-passkey flow additionally blocks the correct password, refuses last-key removal, signs in with dedicated emergency codes without a password, rejects their reuse and replaced sets, and restores password sign-in only after an explicit disable operation. The ordinary browser run uses a 390 × 844 viewport for recovery and replacement flows. Recordings keep a stable desktop viewport; separate mobile stills use the same 390px width with extra height so the complete controls remain visible. A fresh TOTP time step may require waiting up to 30 seconds. Run the seed again before repeating the suite.

To refresh visual evidence, set FLARE_SECURITY_SCREENSHOTS to an output directory and FLARE_SECURITY_VIDEOS to a separate temporary directory before running the browser script. It captures screenshots as WebP and three completed walkthroughs as two-factor-demo.webm, passkey-demo.webm, and passkey-required-demo.webm. Secrets are hidden by capture-only CSS installed before application scripts: QR codes, manual keys, both recovery-code sets, and sensitive inputs never appear in recorded frames. The capture CSS does not alter the shipped UI. Inspect every resulting image and recording before replacing the canonical sources in docs/images/security/ and .github/assets/security/. Include updated written transcripts in the guide and demos. Keep temporary browser output and failed recordings out of Git.

To check a recent sign-in expiring while password/email edits or an Add a passkey dialog remain open, run this separate regression after reseeding:

sh
node scripts/security/seed.cjs
node scripts/security/verify-proof-expiry.cjs

This check uses the exact public demonstration secret above and an HTTP localhost origin. It performs real authenticator enrollment and recovery sign-in, then adjusts only the demonstration session's authentication timestamp to just beyond five minutes. Server responses are real; elapsed time is controlled. It exercises three open forms:

  1. Enter password changes, expire the recent recovery proof, submit, and verify the code prompt appears without a profile mutation. Complete the proof and verify the password change succeeds.
  2. Repeat for a plain email change, verifying the entered address remains and no update is sent before the restored proof fields are completed.
  3. Open Add a passkey and enter a name while recovery proof is fresh. Expire that proof while the dialog stays open. Verify the name remains, password/code controls appear, and no security mutation is sent. Complete the proof and the virtual authenticator's real WebAuthn registration, then verify the saved passkey retains its name.

Optional FLARE_SECURITY_SCREENSHOTS output includes proof-expired.webp with password values masked. The check changes Alex's email/password and registers a passkey; reseed before another suite or demo run.

For the separate authentication database regression suite, create and migrate a disposable local database named exactly flare_security_test_local (or flare_security_test_ci for CI), then run:

sh
export FLARE_SECURITY_DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_security_test_local'
DATABASE_URL="$FLARE_SECURITY_DATABASE_URL" pnpm exec prisma migrate deploy
pnpm exec vitest run __tests__/auth/security-database.test.ts

Without that variable the database suite is skipped. The guard accepts only PostgreSQL URLs on localhost or 127.0.0.1, using one of the two exact database names above and either no schema parameter or a single schema=public. Other URL query parameters are rejected. CI supplies its dedicated database. These tests cover atomic redemption for both code sets, stale-session fences, enrollment, encrypted secret handling, required-passkey transitions and fallback guards, and recovery for local and SSO-only accounts. They complement the browser ceremonies; neither substitutes for testing actual platform authenticators or a live identity provider.

Sessions and audit browser checks and demos ​

The profile session guide, audit guide, and recorded walkthroughs use real application operations with disposable accounts. They do not rely on production accounts or fabricated audit rows.

Create a disposable local PostgreSQL database named exactly flare_audit_demo using your local PostgreSQL tools. The seed guard accepts only localhost or 127.0.0.1, with schema omitted or set once to public; suffix database names, other schemas, duplicate schema parameters, and other URL query parameters are rejected. Use isolated uploads too. Never point this recipe at an existing application database: the seed replaces its demonstration accounts, clears security rate counters, and disables automatic OCR and OIDC in the fixture configuration.

With application dependencies installed, run from the repository root:

sh
export DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_audit_demo'
export NEXTAUTH_URL='http://localhost:3071'
export NEXTAUTH_SECRET='public-disposable-audit-demo-secret-2026-only'
pnpm exec prisma migrate deploy
pnpm dev --port 3071

Adjust the database role, port, and authentication to match your disposable PostgreSQL server. Open http://localhost:3071/auth/login once so the app initializes its configuration. In a second terminal with the same DATABASE_URL, run:

sh
npm ci --prefix docs/site
cd docs/site
npx playwright install chromium
cd ../..
node scripts/audit/seed.cjs
node scripts/audit/verify-ui.cjs

The deliberately public fixture password is Audit-demo-only-2026!. The accounts are Alex Morgan (audit-demo-alex@example.test, administrator), Jamie Rivera (audit-demo-jamie@example.test), and Casey Chen (audit-demo-casey@example.test). Use FLARE_AUDIT_TEST_ORIGIN to select another localhost application origin; its default is http://localhost:3071. The audit scripts use the documentation Playwright installation and launch the Chromium that npx playwright install chromium downloaded; unlike the invite, navigation, and role checks, they ignore PW_CHROMIUM_EXECUTABLE_PATH. Install Chromium's required system libraries on a new Linux host.

Run the seed before repeating the browser checks. When every check passes, the script prints a PASS: line for each flow and exits with status 0; otherwise it prints the error and exits with status 1. The workflow performs real account/file/security operations and OCR of a generated receipt test image, verifies that recognized text is absent from audit details, leaves demonstration evidence for inspection, and should run only against its disposable server. Discard its database and upload directory after verification. API examples in sessions and audit contracts can be executed in an authenticated console on this same instance.

Capture screenshots and videos in separate runs, reseeding before each. Set only FLARE_AUDIT_SCREENSHOTS to a temporary directory for desktop/mobile stills; for the recording pass, leave that variable unset and set only FLARE_AUDIT_VIDEOS. Element screenshots can briefly resize the browser and spoil a simultaneous recording. The videos keep desktop dimensions; mobile coverage is recorded separately in the stills and browser assertions. Review every image and both recordings for credentials, private data, readable pauses, and the final sign-in screen. Only then replace canonical sources under docs/images/audit/ and .github/assets/audit/. Keep generated copies under docs/site/public/, failed recordings, and temporary browser output out of Git. Update the descriptive transcripts on the demos page when the recorded steps change. The documentation browser checks verify that both recordings load without autoplay and that the new guides fit desktop and mobile viewports.

To verify archives and audit logging together on that same disposable instance, reseed and run the separate integration check:

sh
node scripts/audit/seed.cjs
node scripts/audit/verify-archives.cjs

This uses real requests to upload a source, create and browse a ZIP, download an entry, and extract it. It verifies per-file attribution, denied and failed requests, selected member paths, and exclusion of entry contents from audit metadata, then opens the real administrator log with Category set to archives. Set FLARE_AUDIT_SCREENSHOTS to a temporary directory to capture archive-events.webp; inspect it before replacing the canonical audit guide image. This check uses the same FLARE_AUDIT_TEST_ORIGIN and fixture accounts as the sessions/audit suite. It does not replace the broader archive codec, shared-access, and resource-limit checks above.

Same-tab account and session isolation ​

Use the same disposable flare_audit_demo database and fixture setup above. To run this check on port 3072, start the application with that matching authentication origin in place of the earlier server:

sh
export NEXTAUTH_URL='http://localhost:3072'
pnpm dev --port 3072

Open http://localhost:3072/auth/login once to initialize configuration if needed. In a second terminal with the same DATABASE_URL, reseed and run:

sh
export FLARE_AUDIT_TEST_ORIGIN='http://localhost:3072'
node scripts/audit/seed.cjs
node scripts/audit/verify-account-switch.cjs

This check loads Alex’s profile activity, revokes that exact browser session from a separate authenticated client, and follows Files to the real sign-in form for Jamie in the same tab. It checks that Alex’s activity is absent and Jamie’s activity is freshly loaded. It then remotely revokes Jamie’s session, follows the unvisited Upload link, and signs in as Jamie again, checking the new This browser marker while preserving Jamie’s legitimate earlier login history. A browser-document marker verifies that these transitions retain the same page context instead of clearing it with a full reload.

Requests and sign-ins use the real disposable server. Reserved example IP addresses are deliberately supplied as proxy headers to distinguish fixture sessions; they are not measured device locations or evidence that arbitrary proxy headers are trustworthy. Set FLARE_AUDIT_SCREENSHOTS to a temporary directory to capture account-switch.webp, and inspect it before replacing docs/images/audit/account-switch.webp. The eleven canonical audit screenshots include this check and archive-events.webp; the main session/audit suite captures the other nine. The existing session-management recordings show normal review and revocation; this separate check covers account/session transitions without changing those walkthroughs.

To check responses that arrive after an account switch, run the separate controlled network-delay simulation on the same disposable server:

sh
node scripts/audit/seed.cjs
node scripts/audit/verify-account-switch-delays.cjs

It holds an unchanged real response after Alex’s session revocation has completed, then signs in as Jamie through a second tab’s real form before releasing the response. A second case holds the confirmation-requirements read for Set up authenticator across the same account switch. The second tab reloads after sign-in to trigger normal session synchronization; the original document remains loaded. The checks use native session updates and verify that an earlier response cannot sign out Jamie or start a security change for Jamie. Unexpected follow-up mutations are counted and blocked, causing failure; the script does not fabricate successful operations. The original tab may remain signed out after the second-tab login; the check requires Alex’s panels to disappear and verifies Jamie’s live session separately. These timing simulations complement the primary same-tab check, which does not intercept backend responses. They produce no screenshots or recordings.

Audit database regression checks ​

Create and migrate a separate local PostgreSQL database named exactly flare_audit_test before running the database suite. The guard accepts only localhost or 127.0.0.1; supported URL options are schema=public and connection_limit. These tests erase users, roles, settings, and audit rows in that disposable database, so keep it separate from both the browser demo and an existing instance.

sh
export FLARE_AUDIT_DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_audit_test?connection_limit=1'
DATABASE_URL="$FLARE_AUDIT_DATABASE_URL" pnpm exec prisma migrate deploy
pnpm exec vitest run __tests__/audit

Without FLARE_AUDIT_DATABASE_URL, database cases are skipped while core/API tests still run. The PostgreSQL cases verify commit/rollback handling, transaction connection use, safe settings and role snapshots, bulk filename retention beyond 1,000 files, tag metadata, email-failure outcomes, and avoiding recursive logging. Retention cases cover the cutoff, oldest-first batches, the per-run batch limit, the audit.retention.applied summary, and two clients pruning at once under the advisory lock. The one-connection example also checks that transaction snapshots do not wait on a second connection. These checks complement the rendered browser flows and the session-security tests.

Invite browser checks and demos ​

The invite guide, administrator guide, and recorded walkthroughs use real application operations with disposable accounts. Nothing is simulated: the script creates invites, registers real accounts with them, revokes them, and toggles the Everyone grant.

Create a disposable local PostgreSQL database named exactly flare_invites_demo. The seed guard accepts only localhost or 127.0.0.1, with schema omitted or set once to public; other names, schemas, and URL query parameters are rejected. Never point this recipe at an existing application database: the seed deletes every invite, replaces its demonstration accounts and accounts using @invite-demo.example addresses, clears security rate counters and audit events, closes registration, and removes invite permissions from Everyone.

With application dependencies installed, run from the repository root:

sh
export DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_invites_demo'
export NEXTAUTH_URL='http://localhost:3073'
export NEXTAUTH_SECRET='public-disposable-invite-demo-secret-2026-only'
pnpm exec prisma migrate deploy
pnpm build
pnpm start --port 3073

pnpm dev --port 3073 also works for quick checks; the published captures use the production build. Open http://localhost:3073/auth/login once so the app initializes its configuration. In a second terminal with the same DATABASE_URL, run:

sh
npm ci --prefix docs/site
cd docs/site
npx playwright install chromium
cd ../..
node scripts/invites/seed.cjs
node scripts/invites/verify-ui.cjs

The deliberately public fixture password is Invite-demo-only-2026!. The accounts are Alex Morgan (invite-demo-alex@example.test, administrator) and Jamie Rivera (invite-demo-jamie@example.test, Everyone only). Use FLARE_INVITES_TEST_ORIGIN to select another localhost origin; the default is http://localhost:3073. The script uses the documentation Playwright installation and accepts PW_CHROMIUM_EXECUTABLE_PATH. On a Linux host without Chromium's system libraries, install them with npx playwright install-deps chromium as a machine administrator.

The script turns on Let everyone create invites through Settings, creates a Moderator invite in Users → Invites (checking the Administrator warning), registers Sam Rivera in a signed-out browser, and confirms that the spent link shows Invite unavailable and that closed registration still hides the ordinary sign-up page. As Jamie, it creates invites within the member limits, verifies the API rejects unlimited uses and non-expiring links, and registers Priya Shah on a phone-sized screen. As Alex, it revokes one of Jamie's invites, turns the switch off to confirm member invites pause, checks Invited by in the accounts list, the Invitations permission group in Roles, and the invites audit category, and verifies that audit events contain no tokens. Registration is rate-limited per client address, so each fixture browser sends a different reserved documentation address in X-Forwarded-For; these are not real visitors.

Capture screenshots and videos in separate runs, reseeding before each. Set only FLARE_INVITES_SCREENSHOTS to a temporary directory for the thirteen desktop/mobile stills; for the recording pass, leave it unset and set only FLARE_INVITES_VIDEOS, which records admin-invite.webm, join-with-invite.webm, and member-invites.webm with visible typing. Both modes blur the one-time invite link field; review every frame anyway for tokens, credentials, and private data. Then replace the canonical sources under docs/images/invites/ and .github/assets/invites/ and update the transcripts on the demos page. Discard the database afterward.

Invite database regression checks ​

Create and migrate a separate local PostgreSQL database whose name starts with flare_invites_test_, for example flare_invites_test_local. The tests erase users, roles, settings, invites, and audit events in that database.

sh
export FLARE_INVITES_DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_invites_test_local'
DATABASE_URL="$FLARE_INVITES_DATABASE_URL" pnpm exec prisma migrate deploy
pnpm exec vitest run __tests__/invites

Without FLARE_INVITES_DATABASE_URL, database cases are skipped while the policy and token unit tests still run. The PostgreSQL cases use the real route handlers to cover closed-registration sign-up, role grants and their recheck when a link is used, member limits, the Everyone switch and its permissions, revocation, paused links, concurrent use of a single-use link, rollback of a failed registration, deletion of a creator's account, audit records without tokens, batched permission reads that match single-account reads, and the joined-account index.

The dashboard bar follows each account's permissions. scripts/navigation/verify-ui.cjs checks the real bar on the same disposable instance as the role regression scripts: Flare on http://127.0.0.1:3060, set up with the name Alex, the email alex@example.test, and the public fixture password Roles-demo-only-2026!. Everyone must keep its default grants. Run it after the role scripts finish, not alongside them:

sh
node scripts/navigation/verify-ui.cjs

As Alex, the script checks the four places, the Admin menu's pages, order, and hints, one-click switching while the menu is open, the current-page markers on Roles, keyboard use (Enter, arrow keys, End, and Escape returning focus to Admin), the account menu, the 900-pixel breakpoint, and the phone sheet with its Admin and Account groups. It then creates two roles and three accounts: one with View users and View audit log gets an Admin menu of two, whose Users hint leaves out invites, one with only View audit log gets a plain Audit log link, and a member with neither gets the full bar from 768 pixels and an account menu that opens Profile and signs out. axe checks the bar and every open menu or sheet for serious WCAG 2.1 AA problems, and browser runtime errors fail the run. The script removes its roles and accounts when it finishes, including after a failure.

FLARE_NAVIGATION_TEST_ORIGIN selects another loopback origin, FLARE_NAVIGATION_PASSWORD another fixture password, and PW_CHROMIUM_EXECUTABLE_PATH an installed Chromium. Set FLARE_NAVIGATION_SCREENSHOTS to a temporary directory, with a production build running, to capture admin-menu.webp, account-menu.webp, and mobile-sheet.webp for docs/images/navigation/, along with three images for review only. Captures use twice the pixel density, so the menus stay legible when the handbook scales them down. Inspect them before replacing the canonical copies.

Workspace interaction regression ​

scripts/ui-hillclimb/verify-ui.cjs checks the shared controls and creation flows against a populated disposable local instance. Prepare a .test administrator session, a member named Sam Okafor, at least one uploaded file and folder, and the default roles. Keep its Playwright storage state outside Git. The script refuses non-local origins and an administrator without a .test email.

sh
FLARE_HILLCLIMB_ORIGIN=http://localhost:3195 \
FLARE_HILLCLIMB_AUTH="$PWD/.agent-artifacts/ui-check/admin-state.json" \
node scripts/ui-hillclimb/verify-ui.cjs

Install the handbook's separate dependencies first so its Playwright is available. The suite creates and deletes disposable upload/paste files, opens and cancels account actions, and restores local Settings edits without saving them. It checks folder disclosure and remembered state, removable filters, section history and focus, audit presets, password recovery with closed options, real coarse-pointer switch/calendar controls, tablet tabs, and a 200% layout emulation. The latter uses a 720 CSS-pixel viewport at device scale 2; it is not an operating-system zoom or device test. Results and screenshots stay under .agent-artifacts/visual-polish/qa/ unless FLARE_HILLCLIMB_OUTPUT selects another temporary directory.

Embed checks ​

A share link answers by who asks, so its embeds are checked with real requests. scripts/embeds/verify.cjs asks a running instance for share links the way link-preview fetchers do, with the User-Agent of Discord, X, and WhatsApp, a browser, and a fetcher Flare doesn't know, and checks each answer: the redirect to an image, the preview page's tags, the video's poster and lasting address, the protected and private cases, the share page's tags in its <head>, oEmbed, a rich embed, an account's own embed, personal embeds turned off, and details the instance hides.

It writes to the database directly: it marks setup complete, turns OCR off, deletes and recreates the accounts embeds-demo-alex and embeds-demo-mira with their files, uploads five small files, and edits the saved appearance. So it refuses any database not named exactly flare_embeds_demo on localhost or 127.0.0.1, and any origin on another host. Point a local instance at that database, then run:

sh
DATABASE_URL=postgresql://flare_test@127.0.0.1:5432/flare_embeds_demo \
FLARE_EMBEDS_TEST_ORIGIN=http://localhost:3070 \
node scripts/embeds/verify.cjs

NEXTAUTH_URL on the instance must equal the origin, because preview addresses follow it. The script uses local storage answers; the signed-URL path on S3 is covered by __tests__/embeds/routes.test.ts with a stubbed provider. It checks what Flare sends, not how an app draws it: paste a link into the app itself before promising a result there.

Performance benchmarks ​

scripts/perf/ measures the same requests and page loads against two running builds, so a speed claim comes with numbers anyone can reproduce. It needs a local PostgreSQL, the root dependencies and the handbook's (for Playwright and Sharp), and Chromium as in Browser checks.

Seed one disposable database for each build. The seed accepts only a local database named flare_perf or flare_perf_<side> and refuses one that has accounts. It creates an account with 100,000 files among 120,000, 401 accounts, 300 folders, 200 tags, 5,000 short links, 5,000 pending expirations and 300,000 audit events, and links each file to one of a few generated images under uploads/perf/: 12-megapixel photos of about 2.6 MB and screenshots of 360 to 680 KB. The account's password and API token are public and written in the scripts.

sh
export DATABASE_URL='postgresql://flare_test@127.0.0.1:5432/flare_perf_before'
pnpm exec prisma migrate deploy
node scripts/migrate-config.js
node scripts/perf/seed.cjs

Seed the "before" database with the schema of the commit you compare against, and the "after" database the way an upgrade gets it: a copy of "before" (CREATE DATABASE flare_perf_after TEMPLATE flare_perf_before) migrated with the newer build. Start each production build against its own database, with FLARE_CLIENT_IP_HEADER=x-bench-ip so the upload limit counts the benchmark's simulated clients apart, and share one uploads/ directory between them. A standalone copy as the Docker image runs it is closest to a real instance:

sh
pnpm build
cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public
ln -s "$PWD/uploads" .next/standalone/uploads
cd .next/standalone && PORT=3311 HOSTNAME=127.0.0.1 NEXTAUTH_URL=http://localhost:3311 \
  NEXTAUTH_SECRET="$NEXTAUTH_SECRET" FLARE_CLIENT_IP_HEADER=x-bench-ip node server.js

Then, from the repository root:

sh
node scripts/perf/api.cjs --before http://localhost:3311 --after http://localhost:3312 \
  --runs 15 --out results/api.json
node scripts/perf/browser.cjs --before http://localhost:3311 --after http://localhost:3312 \
  --runs 5 --out results/browser.json
node scripts/perf/chart.cjs --api results/api.json --browser results/browser.json \
  --out .github/assets/perf
  • api.cjs times requests from sign-in to the last byte and removes the files it uploads. Each sample must do all of its work, such as 48 files on a page, the 156 files a search matches, or all 480 pages of the concurrent run, counted against the seed's default sizes. A sample that falls short is an error and its time is left out. The burst of 48 uploads is the exception: it reports how many were stored.
  • browser.cjs loads pages in headless Chromium at 1440 × 900, each in a new profile with an empty cache, under Chrome's "Fast 4G" preset and unthrottled. It reports the time until the content shows (every thumbnail in view, the shared image, the sign-in form) and what was downloaded, by kind.
  • Both alternate between the two servers run by run and report the median with its range. Run them on an otherwise idle machine and read the range before trusting a small difference. --only takes scenario IDs.
  • chart.cjs draws the results as the two pictures a pull request can show, and filmstrip.cjs photographs the library opening on both servers at the same moments. The pictures under .github/assets/perf/ come from the api.json and browser.json beside them.

These are measurements of two builds on one machine with a local database. They compare builds; they don't predict the speed of a particular server, network or storage provider.

Bundled fonts ​

The application serves Inter from public/fonts/inter/Inter-latin.woff2 and Inter-other.woff2, which scripts/fonts/split-inter.py makes from Inter-VariableFont_opsz_wght.ttf in the same folder. After replacing that TTF, rebuild both files and copy the two unicode-range values the script prints into app/fonts.ts:

sh
python3 -m venv /tmp/fonts && /tmp/fonts/bin/pip install fonttools brotli
/tmp/fonts/bin/python scripts/fonts/split-inter.py

The split keeps every character, both variable axes, hinting and all layout features, so text is drawn as the TTF draws it; compare a screenshot of the sign-in page before and after to confirm. The TTF files stay in the repository: the handbook uses them, and custom CSS may load them by address.

Automatic coverage checks ​

npm run check:coverage checks every API route file against the inventory, verifies all named-token operations, scopes, and current role requirements against openapi.json, checks that every permission key is covered in the roles guide, compares the downloadable webhook schema to its canonical version, and checks that supported environment variables appear in the configuration guide. It collects those variables from process.env.NAME and FLARE_* names in the .ts, .tsx, .js, .mjs, and .sh files under lib/, app/, and scripts/, in instrumentation.ts, and in next.config.ts. It lists the email overrides with lib/email/environment.ts, the module the app uses to read them, so a new email setting needs its FLARE_EMAIL_* row in the configuration guide. The check loads that module, lib/integrations/bearer-routes.ts, and lib/permissions/catalog.ts without a bundler, so those three must import nothing but types.

Changes to role and user-management components or lib/permissions/ require administration guidance; changes to the bearer route table in lib/integrations/bearer-routes.ts, which maps API routes to permissions and token scopes, also require the API reference. The gate tests these paths with an isolated Git fixture.

CI also checks each non-merge commit on pull requests and direct pushes to main:

sh
npm run check:changes -- --base BASE_COMMIT_SHA

Replace BASE_COMMIT_SHA with the actual hexadecimal commit SHA you are comparing against. The gate requires related documentation areas to change in the same commit as user, administrator, hosting, or API code. A documentation follow-up commit does not cover an earlier feature commit; amend or reorganize the commits before submitting. It cannot verify semantic completeness; reviewers and agents must still use the coverage matrix in AGENTS.md.

Publish an application release ​

Stable application releases start with a commit on main that updates the root package.json version. The Release workflow builds and publishes AMD64 and ARM64 images to Docker Hub and GHCR, creates their multi-architecture version and latest tags, then creates the matching vX.Y.Z Git tag and GitHub release. Let that workflow create the tag: a pre-existing version tag makes it skip the release. Run the application and documentation checks before pushing the version bump.

Write reviewed release notes in .github/release-notes/vX.Y.Z.md in the same commit, with feature coverage, important defaults and limits, upgrade compatibility, real demonstrations, and deployment instructions. When that file exists, the workflow publishes it verbatim; otherwise it retains the generated commit-list notes. The 3.0 preparation reviewed the endpoint inventory, named-token OpenAPI, request examples, webhook schema, and relevant feature guides against the released implementation; release packaging does not change those contracts.

For attachments, add .github/release-notes/vX.Y.Z.assets.json, an array of { "source": "repository/relative/path", "name": "release-attachment-name.ext" } records. Sources must already be reviewed real screenshots or recordings; keep copied assets out of Git. The preparation script checks missing sources, paths, and duplicate names, then copies the files into a temporary directory. The release action uploads them alongside the notes, so release-download links work when publication completes. Use descriptive alt text and link videos to the handbook's written transcripts. Inspect every selected image and recording for credentials and private data before publishing.

You can verify the attachment preparation locally without creating a tag or release:

sh
node scripts/releases/prepare-assets.mjs 3.0.0 /tmp/flare-3.0-release-assets

This stable workflow updates latest and publishes a stable release. A prerelease requires a separate change to its channel and image handling before a prerelease version is pushed. The documentation workflow rebuilds the stable handbook and its dated archive after the successful release workflow; automatic page provenance continues to come from the selected Git tag.

Publish the site ​

The VitePress deployment guide describes the static hosting model. No database, authentication secret, server-side renderer, or app environment variables are needed for this site.

GitHub Pages ​

The repository includes .github/workflows/docs.yml. On pull requests and pushes it checks and browser-tests the current source, then builds and tests the stable site, release archives, and rolling preview from the same checkout. Pull requests never upload a Pages artifact or deploy. Publishing is opt-in through the repository variable below.

  1. In repository Settings → Pages, choose GitHub Actions as the source.
  2. Add the repository Actions variable DOCS_PAGES_ENABLED with the value true.
  3. The workflow defaults to the repository subpath, such as /Flare/. If you use a custom domain at its root, set the Actions variable DOCS_BASE to / and configure that domain in Pages settings.
  4. Run the Documentation workflow on main, or push a docs change to main.

The build job uploads only docs/site/.vitepress/releases/ as the Pages artifact, and the deployment job reports the actual published URL. The product homepage comes from the publishing checkout; its documentation links open guides from GitHub's latest stable release, even when the workflow runs after a push containing unreleased application changes. /rolling/ comes from the published rolling commit. The project homepage, navigation, and archive tooling can improve independently while application guidance remains tied to its release or rolling commit.

Publishing runs after a successful Release or Rolling Release workflow from this repository's main, when a stable or rolling release is manually published or edited, on pushes to main, and on manual Documentation runs from main. The workflow_run trigger is necessary because tags and releases created with GITHUB_TOKEN do not trigger another push/release workflow. Failed release workflows, draft releases, and other prereleases do not publish docs. A push may build before rolling images are ready; the successful Rolling Release completion rebuilds the website with the newly published rolling commit. Publishing runs are serialized across trigger types so their builds and deployments cannot overtake one another. A failed build or browser check leaves the previous Pages deployment in place; fix the failure and rerun Documentation on main.

Release events, completed release workflows, and manual publishing use the tooling checked out from main. Source checks and release builds share that one checkout, so an intervening commit cannot change the tooling between validation and building. No checkout reference or build artifact from a pull request or another workflow is used to publish. Git credentials are not persisted, and the workflow does not restore or save dependency caches. Only the separate deployment job receives Pages write permissions, after all checks pass.

Any other static host ​

Use the repository as the build source, set the build root to the repository root, and configure:

SettingValue
Node version24
Install commandnpm ci --prefix docs/site
Build commandnpm run build:releases --prefix docs/site
Publish directorydocs/site/.vitepress/releases
Base pathDOCS_BASE=/ for a domain root; otherwise the mounted subpath

The repository root, complete Git history and tags, and GitHub release API access are required during the build. Screenshots and runnable examples come from the selected stable or rolling source directories. Keep GH_TOKEN, if needed, in the host's build environment; it must not be embedded in the site. Deploy only the output directory afterward. Ordinary .html URLs are intentional: deep links work on basic static hosts without a catch-all rewrite. Configure the host to serve index.html for directory URLs and 404.html for missing pages.

For an existing web server, build locally and copy the output into a dedicated static document root. Do not route documentation requests through the Flare application or expose the repository, .env, source maps containing private code, or build credentials.

Releases and older guides ​

The versions and changes page lists dated archives for every published stable release and compares their original documentation sources. Dates in the list come from the release's published_at metadata in UTC. They are separate from source commit dates and website build dates. A rebuild can update the website's presentation without changing the archived application documentation.

The same page offers Open rolling docs (unreleased) as an explicit choice. Rolling pages show Rolling preview · Unreleased, an explanation that they describe a rolling build, and Read stable docs to return to the default handbook. Rolling is never selected automatically or remembered as the homepage default. Its update date uses the rolling release's updated_at, because published_at belongs to the first publication of the reused release. Its commit identifies the actual source; its build date identifies the website build. The rolling path is mutable and does not archive every development commit. Rolling pages request noindex; this limits search discovery and is not access control.

Every page displays its Flare version and the source commit that supplied the documentation, with a link to that exact commit. Build details opens the corresponding build-info.json, including the full hash and UTC build time. Release builds identify their tagged content and publishing source separately. Local previews with modified or untracked source show Uncommitted changes; they must not be presented as an exact clean-commit build. Source archives without Git metadata show that the commit is unavailable (or use GITHUB_SHA when a CI archive provides it).

The preview stamp is regenerated by npm run assets, which runs before dev and build; release builds generate provenance for each archived source. No release label, date, or hash needs to be maintained by hand. Keep release-sensitive notes in the relevant guide and tell users where to see their installed version. A version selector links to the matching archive; do not assume current stable instructions apply to an older installation.

The handbook first shipped in 2.1.0. Version 2.0.0 has its original README and nine engineering guides; earlier stable releases have only their README. The archive presents these as legacy documentation, preserving the coverage that existed at the time. It does not claim that later handbook topics or interactive demonstrations existed in those releases. Historical examples may contain mutable latest image tags or external service links; archive readers should use explicit release tags when reproducing an older setup.

Comparisons show added and removed source lines between the selected releases. Handbook snapshots include docs/site/ Markdown, theme components, and downloadable JSON contracts such as OpenAPI; legacy snapshots include README.md and docs/*.md. Comparing a legacy release with a handbook release therefore shows earlier engineering guides as removed and handbook pages as added. These comparisons help locate changed instructions, but do not replace application release notes, migrations, or upgrade guidance. A moved page can appear as a removal and an addition. Preserve historical source content; update present-day explanations in the version browser or the current relevant guide.

The same-commit documentation gate starts when the policy is introduced; it does not retroactively reject commits that predate the handbook.

If a feature is renamed or removed, update navigation and incoming links in the same change. Keep a short redirect/link page when there are established external links. OpenAPI and example clients must change with the implementation; do not promise a versioning policy the server does not implement.

After preparing the disposable timeline fixtures described above, run FLARE_TIMELINE_TEST_ORIGIN=http://localhost:3064 node scripts/timeline/verify-gallery-layout.cjs. It checks 320–1440px layouts, alignment with the navbar, search and card edges, two phone columns, mobile sort/filter disclosure, date-rail hit-area separation, the fixed Dates dialog remaining visible through a jump, and resizing after a jump to the last file. Set FLARE_TIMELINE_STORAGE_STATE to an existing local fixture browser-state file to reuse a sign-in. The 12,000-file fixture now fits within one 8,000-row window on phones; window rebasing remains covered by the timeline-layout unit tests. It uses the real local server and fixture account; no responses are mocked.