Empty Words Unification Plan

The proposal in one line

Fold deep-truth-guides (prose home + gated library) and deep-truth-engine (induction experiences + reader) into a single site, owned by the React SPA, where the first encounter is the induction, the library is paywalled after three free writings, users hold a pin-and-username identity, and every reading session is tracked into an analytics surface built in the house monochrome terminal aesthetic.

The thesis Empty Words is establishing

Understanding nothing is everything. The machinery of nothing is the root of every other machinery on the site. A reader who has seen their own wanting, their own attention, their own fear, and their own dissolution has crossed the gap. The site exists to walk the reader to that crossing and then to keep the crossing visible so it compounds.

The site is therefore not a blog. It is a induction loop. The map does not work unless the reader has first felt the territory.

What exists today

deep-truth-guides (Jekyll, GitHub Pages)

deep-truth-engine (React 19 + Vite + Three.js, also GitHub Pages)

The engine is already the superset. The merge flows toward the engine, not toward Jekyll.

The single-SPA decision

Three options were considered:

  1. Keep Jekyll for home + library, iframe the engine for induction. Rejected. Two URL bases, two PWAs, split install experience, split analytics. The “mandatory induction before library” rule cannot be enforced cleanly across two domains.
  2. Keep both, redirect home between them. Rejected. Same split-identity problem plus flicker.
  3. Collapse into the engine. Chosen. The engine is already a React SPA with Three.js, has the experiences wired, has a reading view, has PWA support, and has Supabase pencilled into the stack. Everything Jekyll does can be moved into React pages in a week. Nothing the engine does can be ported back to Jekyll without reinventing.

The engine becomes the new /deep-truth-guides/ site (same URL, no link decay). Jekyll repo is retired or kept as an archive of the raw markdown.

Unified flow

    first visit
         │
         ▼
    Gateway (focus dot, timer, mantra fade)
         │
         ▼
    Guide Selector (16 tiles)
         │        ┌──────────────┐
         ▼        ▼              │
    Pick a guide                 │
         │                       │
         ▼                       │
    Induction (Three.js scene)   │
         │                       │
         ▼                       │
    Reveal text                  │
         │                       │
         ▼                       │
    Reading (the guide itself) ──┘  optional; can skip back to select
         │
         ▼
    END OF FIRST-TIME PATH
    ────────────────────────────
    return visit / subscribed user
         │
         ▼
    Home (the Parable page, prose)
         │
         ▼
    Library list (21 writings)
         │
         ├── 3 free writings: open
         ├── 18 paywalled writings: pin-gated
         ▼
    Reading or (optional) Induction for that guide

First-visit is enforced; subsequent visits are optional

On first load (no visited=true in localStorage and no user session), the app routes to the Gateway regardless of URL. The user walks Gateway → Selector → one induction → reveal → reading. At reveal we set visited=true and surface an opt-in to subscribe.

On return, the user lands on Home (the prose parable) with a small top-nav shortcut back to Gateway for anyone who wants to re-center. Library below the prose, same gate mechanic, enriched with pin-login for subscribers.

The gate, evolved

Today the gate is a single shared SHA-256 key. That served as a soft filter for casual visitors. It does not support users, tracking, or subscriptions.

New gate has two modes:

Shared SHA-256 gate is removed (or kept as a founder backdoor for demos).

The three free writings

Selected to signal depth without giving the whole arc.

  1. THE MACHINERY OF ATTENTION — the most universal entry point. Every reader arrives with attention damage.
  2. THE MACHINERY OF DISCIPLINE — the most searched folk concept, corrected at the mechanism level.
  3. THE MACHINERY OF NOTHING — the title that earns the brand. The reader who finishes this one either subscribes or leaves. That sorting is the point.

Rationale: Attention is the gateway drug. Discipline is the search-match. Nothing is the tuning fork. Anyone who finishes Nothing and does not subscribe is not the audience; the filter is working.

The subscription + identity model

What a user is

user {
  id               uuid
  username         text  (unique, case-insensitive, claimed at subscribe)
  pin_hash         text  (argon2id of a 4-6 digit pin; argon2id, not sha256)
  email            text  (for recovery only; optional at v1)
  subscribed_at    timestamptz
  subscription_tier text ('free' | 'reader' | 'founder')
  stripe_customer  text  (nullable; only if paid)
  created_at       timestamptz
  last_seen_at     timestamptz
}

Pin is deliberately short because the threat model is not adversarial credential theft. It is “the person typing this is the one who subscribed.” Pair with device fingerprint + localStorage + rate limit and it is sufficient. If someone sophisticated steals a pin, they get a reading dashboard, not a bank account.

Auth flow

  1. Unsubscribed returning user clicks a locked writing → subscribe modal.
  2. Modal collects: username, pin, (optional) email. Stripe checkout opens in same modal (Stripe Elements). On success Supabase row is created, pin hashed, session token cookie dropped.
  3. Future visits: click Sign In → enter username + pin → session resumes.

Why not Supabase Auth directly

Supabase Auth assumes email/password or OAuth. The project wants a pin, not a password, because a pin fits the mono terminal aesthetic and because the value at stake is low. We use the Supabase Postgres tables directly with a thin auth edge function. Same DB, simpler UX, on-brand.

Why Stripe, not Gumroad or Patreon

One backend. One source of truth for subscription state. Clean webhook model. Native to Supabase integrations.

Tracking: the reading telemetry

Every session emits events. Nothing aggregated client-side that can be derived server-side.

Event shape

reading_event {
  id              uuid
  user_id         uuid | null   (null for free-tier anonymous)
  anon_id         uuid          (cookie-pinned, persists across sessions)
  guide_slug      text          (e.g. 'the-machinery-of-attention')
  event_type      text          ('view_start' | 'scroll_depth' | 'section_dwell' | 'view_end' | 'highlight' | 'share' | 'induction_complete')
  payload         jsonb         (scroll_pct, section_id, dwell_ms, selected_text_hash, etc)
  occurred_at     timestamptz
  client_tz       text
  device          text          ('mobile' | 'tablet' | 'desktop')
  is_standalone   bool          (PWA installed or not)
}

Event taxonomy

Event Fires when Payload
view_start guide mounts guide_slug, referrer
scroll_depth on new 10% threshold crossed pct
section_dwell a heading-bounded section has been on screen >3s section_id, dwell_ms
view_end unmount, tab hide, 30s idle, or standalone close total_visible_ms
highlight user highlights text (reuses existing share.js surface) selection_hash
share user triggers share mark selection_hash, method
induction_complete a Three.js experience finishes its reveal guide_slug, variant

Visible-time is computed with the Page Visibility API + IntersectionObserver so background tabs do not inflate.

Why anon_id before user_id

Free-tier readers still produce signal. We want funnel visibility: how many reached Nothing, how many finished it, how many subscribed within 7 days of finishing it. Anon tracking answers that.

The analytics page

Two audiences, one surface.

Reader analytics (their own)

Every subscribed reader has a personal dashboard. Not a vanity feed. A map of how the mind in front of them is engaging with the work.

Core panels, all monochrome, all terminal-mono, all rendered as ASCII + SVG with zero rounded corners:

  1. The Web — force graph of all 21 machineries. Node brightness = engagement depth. Edge = sections the reader highlighted that link two machineries. Grows over time. Decays with dormancy (brightness fades when a guide hasn’t been re-read in 60 days).
  2. Reading timeline — horizontal strip, one tick per reading session. Thickness = duration. Hover reveals guide slug + minutes. Reads like a seismograph.
  3. Depth meter per guide — vertical bars, one per machinery. Fill height = cumulative minutes. Cap at an honest ceiling (say 90 min) so heavy readers don’t distort the axis.
  4. Section heatmap — for a selected guide, show each section’s total dwell time. The slowest-read sections are the mirror: those are the parts where the reader is working.
  5. Highlight corpus — every passage the reader has highlighted, grouped by machinery. Exportable as markdown. This is their own commonplace book.
  6. Return cadence — calendar view, like GitHub contributions but monochrome. Each cell is a reading day; intensity = minutes. Gaps are visible.
  7. Arc trace — the ordered sequence of guides the reader has moved through. Rendered as a thin line on a small-multiples panel. Shows whether the reader is following curated arcs or wandering.
  8. Induction ledger — which experiences they have completed, with timestamps. Some experiences benefit from repetition (Desire, Habit); the ledger shows whether they returned.

Innovation moves — things a standard analytics page does not do:

Founder analytics (site-wide)

Same design language, aggregate scope:

Founder panel lives at /dashboard and is pin-gated to founder-tier only.

The prose home, inside the SPA

The index.md parable becomes a React page component. It renders as markdown (react-markdown, same renderer as guides) so the source of truth stays in one file. At the bottom of the home, the library section renders as React with:

The gate form is replaced by the sign-in / subscribe modal.

Technical implementation plan

Phased because this is not a one-weekend job.

Phase 1: Engine becomes the site

Phase 2: Supabase + auth

Phase 3: Stripe subscriptions

Phase 4: Telemetry

Phase 5: Reader analytics dashboard

Phase 6: Founder analytics

Risks and open questions

  1. URL continuity. If deep-truth-engine takes over /deep-truth-guides/, the engine’s own /deep-truth-engine/ URL is orphaned. Set a 301 redirect on engine’s GitHub Pages repo to the new location. Link rot in any external references is the cost.

  2. PWA identity collision. The guides site just shipped a PWA at /deep-truth-guides/. The engine has its own PWA at /deep-truth-engine/. If the engine migrates to /deep-truth-guides/, installed users on the current PWA will see the app refresh into the new React bundle on their next cache miss. Manifest id will change. Test: install current PWA, push engine-on-guides, observe whether the app updates cleanly or the user has to reinstall. Likely the latter, which is acceptable given readership is small today.

  3. “Leak analytics page” reference unclear. Ask Ladios to name a specific dashboard he considers the reference point. The innovation moves above are my best reading (mirror quotes, dwell pulse, decay scoreboard, empty panel). If he meant a specific leaked-internal-dashboard aesthetic (Twitter internal, Netflix metrics, Spotify Wrapped-style) the direction shifts.

  4. Enforced first-time induction adds friction. Casual visitors who came via a shared link may bounce before experiencing anything. Mitigation: on shared-link arrivals (URL contains a fragment from share.js), show a short inline gateway (15s instead of 60s) then let them land on the shared passage. The mandatory gateway is full-length only on bare landings.

  5. PIN + username at the door may be too high-friction for first subscribers. Consider: subscribe first with just email + Stripe, then on first return prompt for username and pin with the existing email+stripe_customer pairing as the recovery path.

  6. Pricing is unset. Three tiers sketched (free / reader / founder). Reader tier number is the key decision. Recommended starting point: $5/month reader, free tier as above, founder tier $99/year with access to founder analytics. Easy to adjust.

  7. Content-tier ambiguity. What happens to the free writings when the library grows beyond 21? Do the three stay fixed or rotate? My read: fix them for six months, then rotate quarterly. Keeps the filter working without gaming.

  8. Mandatory first-time experience for subscribers on new devices. When a subscribed reader signs in from a new device, do they have to re-do the gateway? Probably no, their account carries the has_inducted flag. But design the flag per-guide-induction so that the mastery tracking remains per-induction-attempt.

  9. GDPR / tracking disclosure. The telemetry is clear-eyed enough that a short privacy note is owed on the dashboard onboarding. One screen, plain language. Do it right.

  10. What happens to the existing SHA-256 shared key? Today’s active readers know the word. On cutover, that word stops working. We should send a heads-up email (for those who have signed up for updates, which we do not have yet because no email capture). Alternate: keep the shared key as an override for 30 days, then retire.

Decision surface

Lock these before code starts:

One-line brand update

The site’s descriptor shifts from:

Mechanistic field notes on how the mind actually works.

to:

Empty Words. Understanding nothing is everything. Induction, then map.

This pairs with the merged flow. The induction is what lets the map make sense. The machinery of nothing is the name for what crossing feels like.

Source