# The Firewriter Owner's Manual

*Version 0.1.0 — 2026-04-16*

You bought a writing app that shoots back. This is the document that makes it yours.

---

## Part I — What you now own

### 1. The thesis

Firewriter exists because most writing tools make deletion free. Free deletion is the problem. Every keystroke you erase is a choice you didn't commit to, and over a long enough session those un-commitments compound into paralysis.

Firewriter makes deletion audible. Every Backspace fires a synthesized gunshot and cracks the canvas in front of you. You can still delete. You just can't delete quietly.

Forward-only writing isn't a gimmick. It's a training aid for commitment. The tool is honest: you may use it to write beautiful rough drafts, not beautiful final ones.

### 2. What's in the box

- **The code** — one Cloudflare Worker (Hono + TypeScript), one D1 database, static frontend (vanilla HTML/JS/CSS, no build step)
- **The schema** — five tables, one migration file
- **The audio engine** — fully synthesized via Web Audio API, zero audio assets to host
- **The canvas** — bullet-hole rendering, crack propagation, debounced shot-firing
- **This manual** — the document in front of you
- **The transfer protocol** — see Part VI

### 3. What's *not* in the box

- Brian's customer list. You bring your own users.
- Brian's Stripe account. You bring your own payments.
- Brian's Cloudflare account. You bring your own hosting.
- Brian's domain (`firewriter.lol`). You bring your own, or negotiate.
- SOC2, HIPAA, or any compliance work. You do that yourself if you need it.
- An SLA. This is source code, not a service.

---

## Part II — The three objects

Firewriter has exactly three nouns. Learn them and the rest of the code reads itself.

### 4. The Weapon

A **weapon** is the sound a shot makes. Six knobs, stored as JSON in `weapons.shot_json`:

| Knob   | What it does                                         |
| ------ | ---------------------------------------------------- |
| `crack`| Initial attack transient — how sharp the shot is     |
| `body` | Mid-frequency fullness — how much "boom" under it    |
| `tail` | Reverb-like decay length in ms                       |
| `pitch`| Frequency center of the body oscillator              |
| `vol`  | Master volume (0–100)                                |
| `var`  | Random variance on each shot — no two identical      |

Each user has **one active weapon**. Saving a new one deactivates the old. The weapon name rides along on every saved writing session, so history shows which weapon produced which output.

To add a new weapon class (e.g., a "rifle" vs "pistol" profile), extend the `shot` object in `/public/armory.html` and the synth chain in `/public/firewriter.js`. The schema doesn't care what keys are in the JSON blob.

### 5. The Range

A **range session** is one writing attempt, bounded by a timer. The query string `?t=600` means "600 seconds on the range." The state machine is:

```
idle → armed (weapon loaded) → live (timer running, shots allowed)
     → completed (timer hit zero → save) OR abandoned (closed tab → vanish)
```

**Failed sessions are never persisted.** This is a feature. `/api/session` is only called when the timer hits zero. If you close the tab, navigate away, or crash, the draft dies with the session. Don't "fix" this.

Shots are debounced at 180ms. Correcting `teh → the` fires one shot, not three.

### 6. The Record

A **record** is a completed session stored in `writing_sessions`. It captures:

- `duration_seconds` — how long you actually wrote
- `target_seconds` — how long you committed to
- `word_count` — final words
- `shots_fired` — how many deletions you made anyway
- `weapon_name` — which profile was loaded
- `text` — the raw output

Records are write-once. There is no edit endpoint. If you want to add one, add it — but consider whether editing a range session violates the thesis.

---

## Part III — Standing it up (zero to live in 30 minutes)

### 7. Prerequisites

- A Cloudflare account with Workers enabled
- A domain (can be any — you'll point it at the Worker)
- A Resend account (optional — for magic-link email; firewriter logs links to console without it)
- `bun` installed locally (`curl -fsSL https://bun.sh/install | bash`)

### 8. The 30-minute deploy

```bash
# 1. Get the code
unzip firewriter.zip && cd firewriter
bun install

# 2. Authenticate Cloudflare
bunx wrangler login

# 3. Create your D1 database
bunx wrangler d1 create firewriter
#    Paste the returned database_id into wrangler.toml

# 4. Run migrations
bun run db:local
bun run db:remote

# 5. Set the email secret (or skip for dev)
bunx wrangler secret put RESEND_API_KEY

# 6. Set the owner email (gates /ops dashboard)
bunx wrangler secret put OWNER_EMAIL
#    Enter the email you'll sign in with

# 7. Update APP_URL in wrangler.toml to your domain

# 8. Ship
bun run deploy

# 9. Wire your domain in the Cloudflare dashboard
#    Workers & Pages → firewriter → Settings → Triggers → Custom Domains
```

That's it. You're live.

### 9. Local dev

```bash
bun run dev
```

Serves everything on `http://localhost:8787`. Without `RESEND_API_KEY` in `.dev.vars`, magic-link URLs are logged to the terminal — copy the URL and paste it in your browser.

---

## Part IV — Configuration reference

### 10. `wrangler.toml` — every key explained

| Key                    | What it does                                                                           |
| ---------------------- | -------------------------------------------------------------------------------------- |
| `name`                 | Worker name as shown in CF dashboard. Also appears in the default subdomain.           |
| `main`                 | Entry point. Don't change unless you restructure `src/`.                               |
| `compatibility_date`   | Pins the Worker runtime to a specific date's behavior.                                 |
| `compatibility_flags`  | `nodejs_compat` enables Node-style APIs we use in dependencies.                        |
| `[assets]`             | Serves `./public` as static files. `not_found_handling = "single-page-application"` makes client-side routes fall back to `index.html`. |
| `[[d1_databases]]`     | Binds your D1 database as `env.DB` inside the Worker.                                  |
| `[vars]`               | Non-secret env vars, baked into deploys. Visible to anyone with CF dashboard access.   |

### 11. Secrets — every one listed

| Secret           | Required? | Rotation cadence | How to set                                      |
| ---------------- | --------- | ---------------- | ----------------------------------------------- |
| `RESEND_API_KEY` | For real email | 90 days | `bunx wrangler secret put RESEND_API_KEY`       |
| `OWNER_EMAIL`    | For `/ops` access | On handoff | `bunx wrangler secret put OWNER_EMAIL`          |

### 12. Email — the magic link

Firewriter's only outbound email is the magic-link sign-in. Without it working, nobody can log in.

#### Your three options

| Provider                   | Status (as of 2026)           | Free tier            | Best for                             |
| -------------------------- | ----------------------------- | -------------------- | ------------------------------------ |
| **Resend**                 | GA, stable                    | 3,000/mo, 100/day    | **Recommended.** What firewriter ships wired to. |
| **Cloudflare Email Service** (`send_email` binding) | Public beta — April 16, 2026 | Unpublished; paid Workers plan required | The future. Revisit at GA. |
| MailChannels (free via CF) | Ended June 30, 2024           | —                    | No longer viable.                    |

**Why Resend:** GA, mature, well-documented, clean DX, deliverability is solid out of the box. The code ships wired to it.

**Why not Cloudflare Email Service yet:** as of 2026-04-16 it just moved from private to public beta. Pricing is still unpublished and breaking changes are expected. When it goes GA with published pricing, migration is a 30-minute job — the Worker binding replaces the `fetch('https://api.resend.com/emails')` call in `src/index.ts`.

**Why not MailChannels:** the free-for-Workers deal ended June 2024. The paid successor exists but there's no reason to prefer it over Resend.

#### Setting up Resend — the 15-minute walkthrough

**Prerequisites:** your domain is on Cloudflare DNS (it is, if firewriter is running). You have an email address you can check.

1. **Create a Resend account.** Go to [resend.com](https://resend.com), sign up, verify your login email. Free tier is fine.

2. **Add your domain.** In the Resend dashboard: *Domains → Add Domain → enter `firewriter.lol`* (or whatever your domain is). Pick a region close to your CF region (us-east-1 is usually fine).

3. **Copy the DNS records Resend gives you.** It will show 3–5 records:
   - **SPF** — a TXT record at the root (or `send.yourdomain`)
   - **DKIM** — typically one TXT record at a subdomain like `resend._domainkey`
   - **MX** (sometimes) — only if you want bounce handling
   - **DMARC** (recommended) — TXT record at `_dmarc.yourdomain`, policy `p=none` to start

4. **Paste them into Cloudflare DNS.** In the CF dashboard: *your domain → DNS → Records → Add record*. For each record Resend gave you:
   - Match the type exactly (TXT, CNAME, MX)
   - Match the name exactly — CF will strip `.yourdomain.com` for you, so enter just the prefix (e.g., `resend._domainkey`)
   - Match the value exactly — no trailing dots, no extra quotes
   - **Turn proxying OFF** (gray cloud, "DNS only") — email records don't proxy

5. **Verify in Resend.** Back in the Resend dashboard, click *Verify DNS Records*. Usually instant; occasionally takes ~5 minutes. If it fails, re-check the records for typos — the most common mistake is double-wrapping the value in quotes.

6. **Create an API key.** In Resend: *API Keys → Create API Key → "Sending access only" → firewriter → Create*. Copy the `re_...` value **immediately** — you can't see it again.

7. **Set the secret on your Worker:**
   ```bash
   cd firewriter
   bunx wrangler secret put RESEND_API_KEY
   # Paste the re_... value when prompted
   ```
   Secrets take effect immediately — no redeploy needed.

8. **Match the FROM_EMAIL to your verified domain.** Open `wrangler.toml`. The `FROM_EMAIL` var should be `noreply@yourdomain` where `yourdomain` is what you verified in step 5. If you change it, run `bun run deploy`.

9. **Test end-to-end.** Visit your live site, request a magic link to an email you control (Gmail is a good test — spam filters are strict). You should get an email within 10 seconds. If not, see the troubleshooting table below.

#### Troubleshooting

| Symptom                              | Most likely cause                                                                 |
| ------------------------------------ | --------------------------------------------------------------------------------- |
| No email, no error in logs           | `RESEND_API_KEY` not set → code falls back to `console.log`. Run `wrangler tail` during a login attempt — if you see `[dev] magic link for ...`, the secret is missing. |
| Email in spam                        | DKIM not verified, or `FROM_EMAIL` domain doesn't match what you verified.        |
| 403 from Resend                      | API key is typed wrong or was revoked. Regenerate and re-put.                     |
| 422 from Resend                      | `FROM_EMAIL` uses a domain you haven't verified. Check wrangler.toml matches.     |
| "Token expired" clicking the link    | The magic-link TTL is 15 min. User waited too long — request a new one.           |
| Works for you, fails for one user    | Their email provider is blocking. Check `postmaster.yourdomain` tools for bounces. |

#### Rotating the Resend key

Every 90 days, or sooner if compromised:

```bash
# Generate new key in Resend dashboard (keep old active)
bunx wrangler secret put RESEND_API_KEY
# Paste new key
# Test signin works
# Delete old key in Resend dashboard
```

#### When Cloudflare Email Service goes GA

Migration is three changes:

1. Replace the `fetch('https://api.resend.com/emails', ...)` call in `src/index.ts` with `await env.EMAIL.send({ to, from, subject, text })`
2. Add an `[[send_email]]` binding to `wrangler.toml` with your verified sender domain
3. Remove the `RESEND_API_KEY` secret

Keep an eye on the [Cloudflare blog](https://blog.cloudflare.com) for the GA announcement.

---

## Part V — Operating

### 13. Day 1 (launch day)

- [ ] Verify `/api/health` returns `{"ok":true}`
- [ ] Sign in with your owner email, confirm magic link arrives
- [ ] Tune a weapon in `/armory`, complete a test session
- [ ] Confirm session appears in `/history`
- [ ] Load `/ops` — the dashboard should render; non-owners get 404
- [ ] Tail logs: `bunx wrangler tail` — run through the flow, confirm no errors

### 14. Day 7

- [ ] Check `/ops` Live pane — any errors accumulated?
- [ ] Query D1 directly to spot-check integrity:
  ```
  bunx wrangler d1 execute firewriter --remote --command "SELECT COUNT(*) FROM users"
  ```
- [ ] Rotate nothing yet — just confirm things are quiet

### 15. Day 30

- [ ] Review `writing_sessions` table size — is growth what you expect?
- [ ] Check CF usage dashboard — are you near any limits?
- [ ] First user feedback — any recurring complaints?
- [ ] Consider: do you need rate limits yet? (See §18.)

### 16. Playbooks

Each is one page. Copy-paste ops.

**Rotate `RESEND_API_KEY`:**
```bash
# Generate new key in Resend dashboard
bunx wrangler secret put RESEND_API_KEY
# Paste new key when prompted
# Old key remains valid in Resend until you delete it there
# Delete old key in Resend after confirming new one works
```

**Export a user's data (GDPR):**
```bash
bunx wrangler d1 execute firewriter --remote \
  --command "SELECT * FROM writing_sessions WHERE user_id = (SELECT id FROM users WHERE email = 'user@example.com')" \
  --json > user-export.json
```

**Delete a user (GDPR):**
```bash
bunx wrangler d1 execute firewriter --remote \
  --command "DELETE FROM writing_sessions WHERE user_id = (SELECT id FROM users WHERE email = 'user@example.com');
             DELETE FROM weapons WHERE user_id = (SELECT id FROM users WHERE email = 'user@example.com');
             DELETE FROM sessions_auth WHERE user_id = (SELECT id FROM users WHERE email = 'user@example.com');
             DELETE FROM users WHERE email = 'user@example.com';"
```

**Back up D1:**
```bash
bunx wrangler d1 export firewriter --remote --output=backup-$(date +%Y%m%d).sql
```

**Restore D1 (to a new instance):**
```bash
bunx wrangler d1 create firewriter-restored
bunx wrangler d1 execute firewriter-restored --remote --file=backup-YYYYMMDD.sql
# Update database_id in wrangler.toml, redeploy
```

**Rollback a bad deploy:**
- CF dashboard → Workers & Pages → firewriter → Deployments → Revert
- Or: `git checkout <prev-commit> && bun run deploy`

---

## Part VI — Selling it to your own users

### 17. Pricing — three templates with my opinion

| Model          | Good fit for                                   | My take for firewriter                         |
| -------------- | ---------------------------------------------- | ---------------------------------------------- |
| One-time       | Indie/prosumer, no server overhead per user   | **Recommended.** Matches the tool's soul.      |
| Subscription   | Ongoing features, community, cloud storage   | Works if you add a writer-community layer.     |
| Credit-based   | Usage-heavy (AI calls, etc.)                  | Only if you ship Claude synthesis post-session.|

### 18. Rate limits you'll eventually need

- `POST /api/auth/request` — 5 per email per hour. Easy abuse vector.
- `POST /api/session` — 1 per user per minute. Prevents replay.
- `PUT /api/weapon` — 60 per user per hour.

Implementation left to you — CF has built-in rate limiting in the WAF tab, or add a KV-backed counter in the Worker.

### 19. TOS / Privacy / Refund templates

See the `/templates/` directory shipped alongside this manual. Fill in your business name. Don't just copy Brian's; your jurisdiction may differ.

---

## Part VII — The handoff economy

### 20. Upgrade protocol

Brian ships v0.2. To pull:

```bash
git remote add upstream <brian's repo url>
git fetch upstream
git merge upstream/main
```

Breaking changes are flagged at the top of `CHANGELOG.md`. If you've modified files Brian also touched, resolve conflicts in your favor — your fork is yours.

### 21. Transfer protocol (you sell firewriter to someone else)

- [ ] Export D1 data (see §16)
- [ ] Buyer sets up their CF account, their D1, their Resend
- [ ] Buyer imports your D1 export into their D1
- [ ] Buyer updates `APP_URL`, sets own secrets, deploys
- [ ] DNS cutover on the domain (or buyer gets their own domain)
- [ ] Email active users from buyer's address: "New owner: X. Same tool."
- [ ] Delete your deployment in CF dashboard
- [ ] Transfer Stripe customers via Stripe's migration flow
- [ ] Shake hands, log off

### 22. License

See `LICENSE.txt`. Short version: you may modify, deploy, and resell firewriter under your own brand. You may not resell *the source code* as-is unless you license that explicitly from Brian.

---

## Appendix A — Schema reference

```sql
users              — one row per signed-up email
magic_tokens       — unburned, 15-min-TTL login tokens
sessions_auth      — active browser sessions (30-day TTL)
weapons            — one active row per user + their history
writing_sessions   — completed range sessions only
```

All tables use `INTEGER NOT NULL` for timestamps (ms since epoch).

## Appendix B — API reference

| Method | Path                     | Auth     | What it does                                   |
| ------ | ------------------------ | -------- | ---------------------------------------------- |
| POST   | `/api/auth/request`      | none     | Send magic link                                |
| GET    | `/api/auth/verify`       | token    | Complete sign-in                               |
| POST   | `/api/auth/logout`       | session  | Destroy session cookie                         |
| GET    | `/api/me`                | optional | Current user or `{user:null}`                  |
| GET    | `/api/weapon`            | optional | Active weapon (or Default)                     |
| PUT    | `/api/weapon`            | required | Save new weapon, deactivate prior              |
| POST   | `/api/session`           | required | Persist completed range session                |
| GET    | `/api/sessions`          | required | List user's session metadata                   |
| GET    | `/api/sessions/:id`      | required | Full text of one session                       |
| GET    | `/api/health`            | none     | `{ok:true,time:...}`                           |
| GET    | `/api/ops/manifest`      | owner    | Codebase manifest + git metadata               |
| GET    | `/api/ops/live`          | owner    | Live DB counts + recent activity               |
| GET    | `/api/ops/file`          | owner    | Source of a manifest-listed file               |

## Appendix C — Changelog

- **v0.1.0** (2026-04-16) — Initial release. Relocated to standalone repo from Manhattan monorepo. Added `/manual` (public), `/ops` (owner-gated).

---

*This manual ships with the code. Read it once. Keep it near.*
