Add a marketing landing page for inputhalo.freno.me with a hero section
(theme-aware app icon, glitch title, tagline), feature highlights, and a
download section offering a macOS DMG via the tRPC signed-S3 URL endpoint
plus an App Store link.
Extract the download orchestration into a pure, side-effect-free helper
(query/performInputHaloDownload) so the asset-name and redirect contract is
unit-tested in isolation, mirroring the existing site-aware page-head /
nav-config split. Include dark and default app-icon exports in the public
folder.
Refactor the monolithic main-site contact page into a reusable
<ContactForm> component that encapsulates the Turnstile widget, cooldown
timer, email-verification flow, and tRPC submission. The main
/contact route becomes a thin wrapper that supplies the site-specific
disclaimer subline and Life-and-Lineage FAQ accordion (now exported as
<LineageContactQuestions> for reuse).
Add per-subdomain contact routes (gaze, inputhalo, lineage, nessa) that
render the shared component, with site-aware subject prefix, recipient,
heading, and PageHead metadata derived via useSite() from CONTACT_CONTEXT.
Introduce src/lib/contact-config.ts (with tests) centralizing the contact
recipient/sender addresses and subject building. The misc.sendContactRequest
mutation accepts an optional subjectPrefix (defaulting to "freno.me") so
inbound call sites continue emitting byte-identical legacy subjects, while
subdomain submissions route distinct subjects (e.g. "[Gaze] Contact Request")
to the same inbox.
Consolidate privacy policies per-subdomain:
- Migrate Gaze and Life and Lineage privacy policies verbatim from the
legacy /privacy-policy/* routes to their subdomain paths
(gaze.freno.me/privacy, lineage.freno.me/privacy), and replace the old
routes with 308 permanent redirects to preserve SEO equity and stable
link resolution.
- Add net-new privacy policies for the InputHalo and Nessa subdomains.
- Update the contact page link to point at the Lineage subdomain privacy
policy.
Move the Gaze marketing home from the legacy /marketing/gaze route to
a dedicated gaze subdomain landing page (src/routes/gaze). The new page
adds a richer hero, feature highlights, and a direct .dmg download flow
that resolves a signed S3 URL via the tRPC downloads.getDownloadUrl
endpoint.
Extract the download-resolution logic into a shared, testable
downloadAsset helper (src/lib/download-asset) so current and future
subdomain landing pages (InputHalo, Lineage, …) share one code path.
The helper is pure over an injected DownloadApi + redirect sink, keeping
it unit-testable without importing solid-js / CSRF cookie code.
Replace the old marketing/gaze route with a permanent 308 redirect to
https://gaze.freno.me so existing inbound links keep resolving to the
canonical home.
Add unit tests covering the tRPC call shape, redirect sink, and error
handling.
Move the marketing landing page content to the new route group
served at , and convert the legacy
route into a 308 permanent
redirect to the new subdomain.
- Add hosting the migrated marketing page.
- Extract shared landing content and the redirect target into
, with unit tests asserting the
redirect destination.
Add the Nessa subdomain landing page served at nessa.freno.me/.
- index.tsx: hero, feature highlights, CTA band, and footer themed via
useSite() brandColor and useDarkMode(); deliberately avoids freno.me
web-auth UI (Nessa uses Clerk) and uses a non-fabricated 'Coming soon'
CTA since there's no app-store presence yet.
- meta.ts: deterministic PageHeadProps (title, description, OG) as a pure
module mirroring the page-head-meta testability pattern; feature copy is
derived from real nessaCommunityRouter capabilities (clubs, challenges,
social.feed, events).
- meta.test.ts: resolves metadata over SITE_CONFIG.nessa + pathname '/' to
assert title suffix composition, canonical, OG image fallback, and
description content.
- Extract shared SubdomainNavList used by both subdomain bars
- Add branded Typewriter header to MainLeftBarContent
- Update BACK_TO_FRENO label to 'back to freno.me'
Refactor LeftBar/RightBarContent into main-site and subdomain variants.
Subdomain sites render a simplified, brand-colored shell iterating a new
pure nav-config module (NAV_CONFIG), with a "back to freno.me" affordance
and inline SVG icon resolver. The main site retains its bespoke bars
(Recent Posts, auth-aware nav, admin links, widgets) unchanged.
Add nav-config.ts as an import-free, unit-testable module defining the
per-site link sets (with external/href/auth-gating/icon contract), plus
filterNavByAuth helper and BACK_TO_FRENO link.
Extract pure resolvePageHeadMeta helper that derives title suffix,
canonical URL, and OpenGraph fallbacks from the active site config and
router pathname. PageHead now reads useSite() + useLocation() to
render per-subdomain metadata (e.g. " | Nessa", canonical on
nessa.freno.me) instead of hardcoding " | Michael Freno".
- Always emit a canonical link (explicit override wins; otherwise
https://<site.domain><pathname>)
- Default og:image to the site ogDefaultImage when not provided
- Add unit tests covering all five sites, canonical derivation, and
OpenGraph fallback behavior
Refactor sitemap.xml route to read the Host header, determine the active
site, and produce a sitemap scoped to that site's routes with canonical
URLs. Extracts route definitions and XML generation into shared modules.
- Add Subdomain Routing section to AGENTS.md with route placement, site context,
API routing, and auth boundary guidance for future tasks
- Add scripts/verify-subdomains.sh for post-deployment verification of DNS, HTTPS,
appcast regression, and SSL certificates
- Document Google Domains CNAME records for all four subdomains
- Document Vercel project domain additions
- Document vercel.json rewrite architecture (api pass-through ordering)
- Include verification checklist and auth boundary notes
Resolve the active site from the request Host header on the server and
expose it through a SiteContext provider so the app can vary rendering
per subdomain. Serialize the resolved site into the document shell
(data-site attribute + window.__SITE__) so client hydration matches the
server render. Add host-based Vercel rewrites mapping each subdomain to
its site root, and include unit tests for the site resolution logic.
The webhook handler now derives emailVerified from the primary email's
Clerk verification status (1 = verified, 0 = unverified) and writes it
on both user.created upserts and user.updated mutations. This aligns
manual test-user creation (scripts/create_test_user) with webhook-created
users, which previously diverged (manual set emailVerified=1, webhook
did not set it at all).
- resolveEmailVerified(): new helper reads the primary email's
verification.status from the Clerk payload.
- INSERT/UPDATE SQL now includes emailVerified in the column list.
- Tests assert emailVerified=1 for verified emails and emailVerified=0
for unverified emails.
- src/server/nessa-auth.ts: replace jose HS256 sign/verify with Clerk
session JWT verification via @clerk/backend verifyToken (RS256/JWKS).
signNessaToken removed — frontend now supplies Clerk session tokens.
- src/server/api/utils.ts: createTRPCContext verifies Clerk JWT, resolves
ctx.nessaUserId via SELECT id FROM users WHERE clerkUserId=? on the
shared NessaConnectionFactory. Lookup miss throws typed UNAUTHORIZED
(webhook has not run yet). Invalid/expired tokens are swallowed; the
enforceNessaUser middleware rejects null nessaUserId.
- src/server/api/routers/nessa-community-authz.test.ts: add clerkUserId
lookup tests (seeded match, missing row, mismatched id, local≠clerk).
- src/server/nessa-auth.test.ts: verifyNessaToken unit tests with mocked
@clerk/backend (valid sub, missing sub, malformed/expired/wrong-signature
rejection) plus static audit that signNessaToken is gone.
- src/server/clerk-user-webhook.ts + src/routes/api/clerk-webhook.ts:
Clerk user.created/user.updated webhook handler (Svix signature
verification, idempotent upsert by clerkUserId, lazy ALTER TABLE
migration) with full test suite.
- src/server/api/routers/nessa.ts: remove legacy register/login/google/
apple sign-in mutations (Clerk is now the sole identity provider).
- src/env/server.ts: add NESSA_CLERK_SECRET, NESSA_CLERK_JWT_ISSUER,
NESSA_CLERK_WEBHOOK_SECRET; NESSA_JWT_SECRET moved to optional.
- package.json: add @clerk/backend, svix; lineage/auth.test.ts and
nessa-ownership.test.ts: add Clerk env vars to env mocks.
- .env.example: document Clerk config vars and rotation.
- delete nessa-google-oauth.test.ts (Google auth removed).
ctx.nessaUserId remains the local users.id — router bodies are untouched.