swyx's personal site
swyx's personal site, using:
- SvelteKit 2 + Svelte 5
- Tailwind 3 + Tailwind Typography
marked+shikifor markdown rendering (replaced mdsvex/remark)- Cloudflare Workers with Static Assets (hybrid: prerendered static pages + on-demand SSR posts, edge-cached)
- GitHub Issues as CMS
Architecture / rendering
- Static (prerendered at build):
/about,/portfolio,/subscribe. - Dynamic + edge-cached:
/,/ideas,/podcasts,/[slug],/rss.xml,/sitemap.xml,
/og/, and selected /api/ routes.
Rendered on demand on Cloudflare Workers and stored in the Cache API via src/hooks.server.js
until s-maxage expires, so new posts appear without a rebuild and serving is O(1).
Versioned OG responses and public read-count GETs retain their public cache headers; most other
cached dynamic responses are returned to the browser as private, no-store to avoid an
additional cache layer outside the Worker.
- Durable content manifest: the Worker reads the parsed GitHub Issues CMS data from the
CONTENT_MANIFEST KV namespace. GitHub is only queried to bootstrap an empty namespace or
refresh it after a webhook, so ordinary cache misses do not depend on GitHub availability.
- Compact ideas list:
/api/listContent.jsonomits article bodies for the default/ideas,
/api/searchContent.json,
which the browser downloads only after a reader uses the search box.
- Instant publishing: a GitHub Issues webhook hits
/api/revalidate, which verifies the
- Owned social cards: every public HTML page points at a versioned, 1200×630 PNG generated by
/og/page/ or /og/article/. Article cards are resolved from the
trusted content manifest and can incorporate an explicit frontmatter image; query parameters
never supply arbitrary card content.
- Approximate read counts: public pages use a 0.5% engaged-read sample backed by D1. Each
- Ephemeral live readers: public pages optionally join a page-scoped, hibernating Durable
- Runtime-fault attribution: a dedicated Tail Worker ignores lifecycle churn and stores only
Environment variables (Cloudflare Workers)
What each variable does
| Variable | Required? | What it does |
| ------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GH_TOKEN | Yes | A GitHub Personal Access Token used to authenticate calls to the GitHub Issues API (the CMS). Without it, requests are unauthenticated and capped at 60/hr, which the site blows through quickly and starts failing. With it, the limit is 5000/hr. Read at runtime via $env/dynamic/private (Cloudflare platform.env) and at build time for the prerendered pages — so it must be set in both the runtime secrets and the build environment. |
| GH_WEBHOOK_SECRET | Recommended | A shared secret used to verify (HMAC SHA‑256) that incoming requests to /api/revalidate actually came from your GitHub webhook. This enables fast publishing: editing an Issue refreshes the KV manifest and rolls the cache generation instead of waiting for the s-maxage TTL. If unset, /api/revalidate returns 500 and you fall back to TTL-based freshness. |
| GA4_MEASUREMENT_ID | Recommended | Public GA4 stream identifier used only by the server-side read-event mirror. Production currently uses G-TW6GTQ9Q4N and declares it as a non-secret [vars] value in wrangler.toml. |
| GA4_API_SECRET | Production | Secret for the GA4 Measurement Protocol stream. It is sent only from the Worker and must never be committed, placed in a URL in source code, or exposed to the browser. The application treats it as optional so D1 counting survives a GA outage, but wrangler.toml requires it for production deployment. |
| GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | Yes | Google web OAuth client credentials for tools sign-in. |
| TOOLS_SESSION_SECRET | Yes | Random signing key of at least 32 bytes; rotation invalidates all tools sessions. |
| TOOLS_OWNER_GOOGLE_SUB | Yes | Exact immutable Google account ID of the site owner, never an email or a first-login grant. |
| GOOGLE_REDIRECT_URI | Yes | https://swyx.io/tools/auth/google/callback; localhost override for local development. |
| PRESENCE_ENABLED | Recommended | Server-side emergency kill switch for new live-reader sockets. Set to false and deploy the main Worker to reject presence while leaving every page usable. |
| PUBLIC_PRESENCE_ADMISSION_RATE | Recommended | Deployment-time browser admission fraction from 0 through 1. Production starts at 1; use 0.1 during a viral spike to reduce socket workload by roughly 90%. This value is public by design. |
Where to get the values
GH_TOKEN— GitHub → Settings → Developer settings → Personal access tokens. A classic token withpublic_repo(orrepofor private) scope is sufficient since it only reads Issues.GH_WEBHOOK_SECRET— generate any strong random string, e.g.openssl rand -hex 32. You'll paste the same value into the GitHub webhook config (below).GA4_API_SECRET— Google Analytics Admin → Data streams → select theswyx.ioweb stream →
Set them on Cloudflare Workers
Option A — Dashboard: Cloudflare dashboard → Workers & Pages → your Worker → Settings → Variables and Secrets. Add each runtime value and encrypt secrets. GH_TOKEN must also be present in the Git-connected build environment because prerendered pages read it during builds.
Option B — Wrangler CLI:
# runtime secrets (encrypted)
npx wrangler secret put GH_TOKEN
npx wrangler secret put GH_WEBHOOK_SECRET
npx wrangler secret put GA4_API_SECRET
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
npx wrangler secret put TOOLS_SESSION_SECRET
npx wrangler secret put TOOLS_OWNER_GOOGLE_SUB
Each command prompts for the value. List them with npx wrangler secret list.
wrangler.toml declares GH_TOKEN, both podcast-admin secrets, and GA4_API_SECRET as required so
deployments warn or fail clearly instead of silently publishing incomplete production
configuration.
It also declares the existing CONTENT_MANIFEST KV, READ_COUNTERS D1, and PODCAST_MEDIA R2
bindings. A fork or new Cloudflare account must create those resources first and replace their IDs
in wrangler.toml; Wrangler cannot recreate resources from another account's IDs.
The PRESENCE_ROOMS binding is different: it points to the separately deployed
swyxdotio-presence Worker. Deploy that Worker before the main site Worker.
Local Wrangler preview reads secrets from a gitignored .dev.vars; ordinary Vite development
can also use.env. Never copy production secret values into README,.env.example, tests, or
shell history. A missing GA4_API_SECRET warning during a local build is expected when analytics
delivery is not under test.
Wire up the GitHub webhook (for instant publishing)
In your content repo: Settings → Webhooks → Add webhook:
- Payload URL:
https://swyxdotio.swyxio.workers.dev/api/revalidate - Content type:
application/json - Secret: the same value as
GH_WEBHOOK_SECRET - Events: "Let me select individual events" → check Issues only
/,
/ideas, /{slug}, /rss.xml, /sitemap.xml, and the list/api endpoints).
Cloudflare resources for a new environment
Create the durable resources before the first deployment, then copy the returned IDs into
wrangler.toml:
npx wrangler kv namespace create CONTENT_MANIFEST
npx wrangler d1 create swyxdotio-read-counters
npx wrangler r2 bucket create swyxdotio-podcast-media
The D1 binding must be named READ_COUNTERS, the database must use
migrations_dir = "migrations/read-counters", and migrations must be applied explicitly:
# Local development database
npx wrangler d1 migrations apply swyxdotio-read-counters --local
Production database
npx wrangler d1 migrations apply swyxdotio-read-counters --remote
Useful verification after migration or deployment
npx wrangler d1 execute swyxdotio-read-counters --remote \
--command "SELECT page_key, read_count, sample_count, sampling_policy_version FROM page_reads ORDER BY updated_at DESC LIMIT 20"
Migration 0002_add_sampling_metadata.sql contains tracked ALTER TABLE statements. Let Wrangler's
migration ledger apply it once; do not copy and execute those statements by hand or rerun the SQL
outside the migration command.
Do not seed historical estimates into D1. D1 is the independently auditable post-launch sample ledger; the historical estimates are a separate static presentation layer.
Live reader presence and sharing
Presence is intentionally playful and approximate. On desktop, admitted readers see ephemeral
country-labelled cursors. On mobile, the persistent representation is a flag bead on a reading
progress rail; a tap or drag adds a temporary passive touch cursor without interfering with native
scrolling. The only room communication is movement, one of 👋 ❤️ 💡 😂 ✨, and a fixed share
sparkle. Highlighted quote text, URLs, destinations, IP addresses, and user agents never enter the
Durable Object.
The browser waits until the page has been visible for two seconds before connecting. The feature is on by default and has a persistent Hide live readers preference. Hidden tabs stop sending; after 30 hidden seconds the socket closes. Rooms admit at most 32 readers, use WebSocket hibernation, and store no application data. Room IDs are resolved from the same finite public-page registry and persisted non-private article manifest used by read counts, so tools, APIs, feeds, errors, private articles, and arbitrary attacker-controlled keys cannot create rooms.
Deploy and develop
The Durable Object is an auxiliary Worker because the SvelteKit adapter owns the generated main
Worker entrypoint. Its declarative SQLite export lives in wrangler.presence.toml; SQLite is used
for the Durable Object class declaration only and the application performs no SQL writes.
# Local: build the SvelteKit Worker, then run both Workers together
npm run build
npm run preview:presence
Production: this order is required for the external binding
npm run deploy:presence
npm run deploy
For a fast production smoke test, open the same public page in two normal browser contexts and
confirm the pill changes from 1 here to 2 here. Then verify hiding persists across reloads,
mobile emulation shows the reading rail, a reaction travels, a highlighted quote opens sharing,
and the browser network panel contains no selected text in WebSocket frames. An overflowed room
closes excess clients with 1013; malformed/abusive frames close with 1008.
Cost envelope and controls
The workload model assumes one admitted socket and an average of 12 compact incoming frames per eligible visit. Hibernated idle sockets are not billed for duration, outgoing WebSocket messages are free, and incoming messages are billed in 20-message request units. Under July 2026 Cloudflare Workers/Durable Objects paid pricing, the planning envelope is:
| Eligible views/day | Estimated total monthly workload cost | | -----------------: | ------------------------------------: | | 25,000 | ~$5.03 | | 1,000,000 | ~$18 | | 5,000,000 | ~$92 | | 10,000,000 | ~$187 |
The first row is mostly the existing $5 Workers plan. These are engineering estimates, not a bill forecast: visit length, motion, cache behavior, and other site Worker traffic can move the result. Do not add application heartbeats; they waste billable incoming messages and defeat hibernation.
Operational thresholds:
- Start with
PUBLIC_PRESENCE_ADMISSION_RATE = "1"andPRESENCE_ENABLED = "true". - If projected incremental presence cost exceeds $25/month or traffic approaches 1M eligible
0.1, build, and deploy the main Worker:
PUBLIC_PRESENCE_ADMISSION_RATE=0.1 npm run deploy.
Keep the matching wrangler.toml value as an operational record. Existing rooms remain useful
while roughly 90% of browsers avoid opening a socket at all.
- For emergency shutdown, set
PRESENCE_ENABLED = "false", rebuild with public admission0,
- Monitor only aggregate Worker/DO requests, active duration,
room-full, malformed-frame, and
Pricing references: Durable Objects pricing, WebSocket hibernation, and multi-Worker local development.
Open Graph image system
The site owns its social images rather than depending on Tailgraph or another hosted renderer:
@ethercorps/sveltekit-ogand its Vite plugin bundle Satori/resvg WASM for Cloudflare Workers.src/lib/social-meta.jsis the registry for the six public page cards and the shared metadata
OG_DESIGN_VERSION whenever a visual change should invalidate social caches.
src/lib/og/contains card inputs, templates, rendering, committed open-licensed fonts, guarded
- The committed fonts are Newsreader Semibold, Noto Sans Regular, and Caveat Semibold. The card
/og/page/[key].png?v=serves home, About, Ideas, Podcasts, Portfolio, and
/og/article/[slug].png?v=resolves only public articles from the-
- Article
image/cover_imagevalues may enhance the template. Only HTTPS JPEG, PNG, and WebP
- Generated PNGs are 1200×630, capped below 5 MB, and cached for one year as immutable. A complete
src/lib/og/assets/notebook-fallback.png with X-OG-Fallback: 1
and a 60-second cache TTL so a transient failure does not poison the card for a year.
- HEAD validates the public card without fetching its cover or rasterizing a PNG. OG cache keys
v values cannot bypass it.
- Cache-miss renders emit bounded
og_renderstart/completion/fallback records with card kind,
cpuTime and wallTime for timing;
the Worker's JavaScript clock can remain fixed during CPU work. Logs contain no card content or URLs.
Every public page should use src/components/SocialMeta.svelte; do not add route-local duplicate
Open Graph tags. Non-article pages use og:type=website, articles use article, and all metadata
must contain absolute HTTPS URLs, dimensions, MIME type, and alt text. Tools, APIs, feeds, private
pages, and errors deliberately do not receive generated cards.
Useful production checks:
curl -I "https://swyx.io/og/page/home.png?v=1"
curl -I "https://swyx.io/og/article/learn-in-public.png?v=spotcheck"
curl -fsS "https://swyx.io/og/page/home.png?v=spotcheck" -o /tmp/swyx-og.png
file /tmp/swyx-og.png
Deployment version and content generation invalidate internal OG cache entries. A fresh version query refreshes downstream social caches, but does not bypass the Worker's internal cache. After deployment, also test a fresh X draft and LinkedIn Post Inspector; previously shared URLs may retain network-owned caches.
The focused unit suite validates registry coverage, metadata versioning, input rejection, image fetch bounds/timeouts, escaping, and Unicode. It does not currently rasterize and snapshot every PNG variant, so production byte/dimension checks and visual inspection remain required after OG template or font changes.
Read counts and GA4
The public counter is intentionally an order-of-magnitude estimate, not a precise analytics
system. The authoritative policy constants are in src/lib/read-counter.js; methodology and
historical backfill notes live in docs/read-counter.md.
Current policy (v1-p005):
- A browser must keep the page visible for 8 seconds; articles additionally require 25% scroll
- A browser/page pair is deduplicated in local storage for 24 hours.
- Eligible reads are sampled at 0.5%. Only sampled clients POST; each accepted sample atomically
sample_count in D1.
- The API validates same-origin requests, rejects obvious bots and arbitrary/private content keys,
- Unsampled engaged readers may GET the public total. The browser remembers the displayed count for
- Counts are visible by default. A reader can hide them globally from any displayed counter; the
- Successful D1 increments are mirrored asynchronously to GA4 as
engaged_read. GA failure,
- Global Privacy Control and Do Not Track suppress GA delivery and identifier creation. The GA
The production GA property is swyx - GA4 (property 391847479, web stream 5667734629,
measurement ID G-TW6GTQ9Q4N). Its event-scoped custom metric is:
- Name:
Estimated reads - Event parameter:
read_weight - Unit: Standard
A separate monthly calibration Worker compares D1 sample deltas with GA4 delivery and stores a
diagnostic report. Its setup, interpretation limits, and production queries are documented in
docs/read-counter.md. In particular, its current session ratio is not an independent estimate of
historical traffic and must not be used to rewrite the static lifetime backfill.
Capacity and cost envelope
The 0.5% policy is deliberately bounded for an expected maximum of 10 million reads/day:
| Engaged reads/day | Sampled POSTs + D1 writes/day | Approx. daily 95% sampling error | | ----------------: | ----------------------------: | -------------------------------: | | 1,000,000 | 5,000 | ±2.77% | | 5,000,000 | 25,000 | ±1.24% | | 10,000,000 | 50,000 | ±0.87% |
This is sampling error only; blocked JavaScript, dropped requests, bots, and privacy choices can create larger systematic differences. At 10 million/day, sampled writes stay below D1 Free's 100,000 writes/day and use about 1.5 million writes/month, within the Workers Paid/D1 Paid included allowances as of July 2026.
The remaining scale risk is the public count GET, not D1: every unique browser/page/day can cause one Worker request even when the response is served from Cache API. At all-unique traffic, the incremental counter-endpoint envelope is roughly $11/month at 1M/day, $47/month at 5M/day, and $92/month at 10M/day under July 2026 Workers pricing, before other dynamic Worker traffic. If the site approaches that range, publish an hourly/daily static count snapshot through static assets or R2/custom-domain caching so readers no longer call the Worker counter endpoint. Do not increase D1 precision merely because traffic rises; rough magnitude is the product requirement.
Operational limitations
- A successful POST does not purge an already cached GET, so another reader may see a total that is
- The browser records its 24-hour dedupe marker before sending the request. A transient failed POST
- Same-origin, user-agent, finite-key, engagement, and weight checks are abuse friction rather than
- D1 failure returns 503 from the counter API while the page itself remains usable; the component
- Sitewide sampling converges quickly at scale, but individual long-tail pages can remain noisy for
Changing the sampling policy
Treat the rate, weight, and policy version as one migration:
- Change
READ_SAMPLE_RATE,READ_SAMPLE_WEIGHT, andREAD_SAMPLING_POLICYtogether. - Keep the server-owned header validation and D1 write weight aligned.
- Update the read-counter unit tests and
docs/read-counter.md. - Preserve existing rows; never rewrite old sampled counts as though they used the new policy.
- Recalculate the capacity table and GA custom-metric interpretation before deploying.
wrangler.toml, upload the secret with wrangler secret put GA4_API_SECRET, and
create the Estimated reads custom metric above. Validate a test event with Google's debug
Measurement Protocol endpoint before relying on Realtime.
Production read-counter checks:
# Public count and caching; the second request should become an edge HIT
spot="readme-spotcheck"
curl -i "https://swyx.io/api/reads/learn-in-public?spot=$spot"
curl -i "https://swyx.io/api/reads/learn-in-public?spot=$spot"
Confirm deployed secret names without printing their values
npx wrangler secret list
Avoid casual production POST tests: every accepted sample intentionally adds 200 displayed reads. If one is necessary, record the before/after D1 values and use a real public key.
Deployment checklist
npm install
node --test tests/*.test.mjs
npm run check
npm run build
npx wrangler d1 migrations apply swyxdotio-read-counters --remote
npm run deploy:presence
npm run deploy:calibration
npm run deploy:monitor
npm run deploy
After deployment:
- Record the Git commit and Worker Version ID.
npm run deployrequires a clean Git checkout,
X-Swyx-Commit.
- Fetch representative HTML, all six page-card endpoints, an article card with and without an
- Confirm PNG signatures/dimensions,
Cache-Control,Content-Type, and absence of
X-OG-Fallback on normal renders.
- Confirm the second identical read-count GET is an edge cache hit and POST responses remain
private, no-store.
- Check D1 rows and GA Realtime
engaged_readindependently. Neither alone proves the other
- If using the hourly monitor, verify that
ops_monitor_snapshotsreceives a fresh row and that
presence_monitor_hourly stays aggregate-only.
- Keep the worktree clean and never commit
.dev.vars, GA API secrets, or Cloudflare tokens.
Commands
npm run dev— local dev servernpm run build— production build (Cloudflare adapter)npm run deploy— build and deploy a clean checkout with its Git commit recorded in Cloudflarenpm run preview— preview withwrangler dev; use this rather than plain Vite when testing
npm run deploy:monitor— deploy the hourly read/presence monitor Workernode --test tests/*.test.mjs— fast unit and contract tests, including OG and read analyticsnode tests/markdown.test.mjs— markdown renderer regression checksnpm test— Playwright e2e (requires GH content)
Live URL
See https://swyx.io
- Netlify to Cloudflare DNS cutover notes
- https://sw-yx.js.org/ old site when learning to code.
- You can see previous iterations of the site from 2017 here: https://www.swyx.io/rewrite-2022
- The last version of the 2022 site was preserved at https://github.com/swyxio/swyxdotio2022
- The 2023 site is documented at https://www.swyx.io/rewrite-2023
Google sign-in and tenant boundaries
Local development
npm run dev automatically provides a Local developer tools account on HTTP
localhost, 127.0.0.1, and [::1]. No Google setup or cookie injection is needed.
The synthetic local-development identity is a regular member with its own
cache/storage namespace, not the production owner or your Google data. Existing
valid sessions take precedence. To test the real signed-out/Google flow, restart
with TOOLS_DEV_AUTH=off npm run dev.
This requires Vite's build-time DEV flag and a loopback URL. It is unavailable
in production builds, deployed previews, LAN hosts, or wrangler dev running a
production build; those previews use normal Google/test-fixture sessions.
Local sign-in does not supply storage bindings or provider keys, and no AI request
starts automatically. Vite keeps remote bindings disabled.
Production sessions
Tools follow Google OpenID Connect through openid-client with PKCE, state, nonce,
issuer/audience/expiry checks and signed ID-token verification. jose signs the
seven-day HTTP-only tools session. No Google access/refresh tokens are retained.
There is no password-login endpoint or legacy session fallback.
/drawand/boxpermanently redirect to/tools/drawand/tools/box.
/tools; browser storage and API paths are unchanged.
/tools/draw: every Googlesubgets its own Durable Object workspace. Only the
personal workspace, preserving production
drawings. Requests cannot supply another tenant’s namespace.
- Browser drawing caches, library, assistant history and generation history are
- Drawing writes require
X-Tools-Usermatching the signed session, so an old tab
/tools/boxtext has no server persistence; signed-in opens have activity metadata.
- Cloud AI is site-funded for every signed-in account, with durable admission
TOOLS_OWNER_GOOGLE_SUB, never an email or client flag.
Owner UI suppresses funding, logging, provider-sharing and budget notices, but
activity remains logged and actual errors and data-loss warnings remain visible.
These estimates are not exact provider billing caps. Failed attempts retain their
reservations. Provider job IDs are bound to their originating Google account.
- AI admission/status logs contain account ID, request ID, model, timestamp, status
GET /tools/api/ai/usage returns only
the current account's counters and policy. No ledger means no paid request.
- Podcast publishing, archive migration and the separately hosted Reclip redirect
Production setup
- Create a dedicated Google Cloud project and an External Google Auth app.
openid, email, profile. No Drive/Gmail APIs or offline access.
- Create a Web application client with redirect URI
https://swyx.io/tools/auth/google/callback. For local OAuth testing register a
separate localhost callback and set GOOGLE_REDIRECT_URI to the exact port.
- Publish the Google app’s audience for general sign-in rather than relying on a
https://swyx.io/tools and privacy
URL to https://swyx.io/tools/privacy; satisfy Google’s domain/brand requirements.
- Store the four required secrets above with Wrangler; never commit/download
sub from a validated sign-in;
never grant admin based on the first user to register.
- Build and test before deploying. Verify a real owner login and a different
- Old password secrets may be removed from provider configuration only after
Auth pages/APIs bypass shared caches, use no-store, and suppress referrer data.
Local tests use signed fixture identities only on localhost, never a production
login bypass. Provider-token validation is covered separately with mocked Google
responses and real test RSA signatures.
Google project
The auth-only project is swyx-io-tools (project number 31511070245), owned by
[email protected]; no billing account is linked. Google branding is **swyx.io
Tools, audience External, client swyx.io Tools Web**. Registered callbacks
are production and http://localhost:4188/tools/auth/google/callback for verification.
Deploy the drawing companion first whenever the AI ledger protocol changes, then
the main Worker. The companion has no public data endpoint.
Tool logs
/tools/logs is the Google-authenticated activity dashboard. Everyone can inspect
their own records; only the current TOOLS_OWNER_GOOGLE_SUB can select Everyone.
Owner access is usage metadata only, not permission to read private drawing assets,
prompts or generated content. The notice and privacy page disclose owner visibility
of account names/emails and metadata to all users.
- Quick views isolate AI, failures, pending requests, or meaningful actions with
- AI admissions/statuses are read directly from the existing quota ledger. They
- Server instrumentation records cloud drawing changes, podcast uploads, and
- Records and inactive account-directory entries are pruned after 30 days by the
GET /tools/api/logsacceptsdays=1|7|30,kind,tool,scope=mine|all,
status (including pending), source, exact model/action, opens=hide,
metadata-only q (request ID/action/tool/model), and a UTC day within the period.
Owner Everyone views can also filter by opaque account ID. Query parameters are
validated and account scope is enforced before every query, aggregate, or export.
Quick views, summary counts, daily activity, and top-20 tool/model/action/account
breakdowns drill into the same bookmarkable filters; totals are never page-only.
GET /tools/api/logs/export?format=csv|jsonaccepts the same filters and requires
X-Tools-User header. Downloads include **all matching
records**, not only the first 50. Views over 10,000 rows return 413 with instructions
to narrow filters; no partial file is generated. CSV cells are spreadsheet-formula
safe; JSON includes filters, coverage, range, count, and cost definitions.
snapshot fixes the creation-time cutoff to the displayed result. Outcomes can
settle after a prior read, and expired records still obey 30-day retention.
Pagination before cursors bind the full filter set, account, and cutoff; they
cannot be used on export routes.
all is server-authorized. Browser POST accepts only {id,action,status},
requires same origin plus X-Tools-User, and gets identity/time/provenance from
the server. User IDs, profile fields and arbitrary payloads are rejected.
Generation metadata stays in the same usage ledger: tools_ai_generation_observations
is a one-row extension keyed by the canonical usage ID, not another job scheduler.
Logs join existing run/client-job IDs, immutable hosting adapter, and provider request ID.
Media views filter adapter, modality, and run; run summaries count **matching admitted
jobs**, not planned/unsubmitted candidates, and never combine the same run ID across accounts.
Catalog cost estimates are distinct from budget reservations and provider bills. Observed
request span and wait-until-running include queue/poll delays, not GPU execution time.
Missing historical estimates/timings stay null; mixed run totals show coverage rather than
inventing zero costs. Cancellation requested/confirmed/unsupported and bounded failure codes
are separate from request outcomes. Polls do not add admissions or move terminal timestamps.
No new inference provider or billing integration is enabled. GPU, cold-start, warm-idle and
actual billed cost remain unavailable. The metadata extension is created additively and pruned
with the existing 30-day usage records; existing quotas and owner exemptions are unchanged.
New tool integrations must extend the bounded vocabulary in
src/lib/tools-activity.js. Use recordToolActivity(userId, action, status) only
for browser-reported actions, and the server activity helpers for authoritative
operations. Never pass prompts, images, page/file names, URLs, cookies, or keys.
The logs API and dashboard never expose another account to ordinary users, even
if they supply an account ID, owner scope, or another account’s cursor.