Skip to content

Moshpit is lifetime pricing: $5 an ending, $2 a name, paid once - #436

Merged
ralyodio merged 1 commit into
mainfrom
worktree-moshpit-lifetime
Aug 28, 2026
Merged

Moshpit is lifetime pricing: $5 an ending, $2 a name, paid once#436
ralyodio merged 1 commit into
mainfrom
worktree-moshpit-lifetime

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Migration 010 gave endings a one-year term with renewals, per PRD 0005 §5. That is withdrawn. The prices are unchanged — what changed is that they are not charged again.

The reason is what the namespace is for. A name that lapses is a name somebody else can catch, and the whole pitch is that you can finally hold the clean name instead of the hyphenated one you settled for. An annual invoice with a drop date attached is the thing people are trying to get away from; selling it back to them undoes the pitch.

Two things worth knowing before reviewing

Names were already sold this way.moshpit_names has never had an expiry column and nothing has ever renewed a name. The PRD called it a yearly fee, the schema sold it outright, and the doc comment described the PRD. It now describes the code.

The ending term never shipped. Nothing in the app could open an ending checkout, so quoteTld and quoteRenewal were unreachable and only the webhook settler was wired up. No ending was ever charged a renewal, and no row in the wild has an expiry this drop takes away. That is why migration 016 is a plain drop rather than a grandfathering policy — §21.8 asks for one before putting endings into a lifecycle, and taking them back out of one nobody was in needs no such thing.

What changed

  • expires_at and term_started_at dropped from moshpit_tlds, columns and index. Gone rather than left NULL: a nullable expiry is an annual term waiting to be switched back on.
  • moshpit_tld_purchaseskeepskind and years. That is a financial record of what was sold at the time, and a ledger is not something to rewrite once the product changes.
  • A renew row can no longer be created, but one may still settle late. It is honoured rather than refused — the buyer keeps the ending, which now keeps itself. Refusing it would take money for nothing. (A renewal for an ending that has since changed hands is still a refund.)
  • isExpired() stays and always answers no, so callers asking a fair question get a permanent answer instead of an import that fails.
  • quoteRenewal, TERM_MS and MAX_TERM_YEARS are gone rather than left as no-ops somebody could wire a checkout to. There is a test asserting they are undefined.

The pit page

Leads with it, since it is the one claim here worth making above the fold and the whole reason to prefer this over a registrar:

Bought once. Yours for good.
$5 an ending, $2 a name — paid one time, not every year. Nothing here renews, nothing lapses, and no name you hold can drop because an invoice went to an address you stopped reading.

For whoever writes the next migration

Nothing may follow the last statement in a .sql file but whitespace. migrate.mjs splits on semicolons and hands each piece to libSQL, and a trailing comment-only piece comes back as SQLITE_OK: not an error, which is as unhelpful as it sounds. Cost me a debug cycle; noted in the migration header.

Tests

591 pass in apps/pwa.moshpit-terms.test.mjs rewritten for lifetime semantics; migration verified from scratch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GWhPX5Uzd29whRg5WPAYM7

Migration 010 gave endings a one-year term with renewals, per PRD 0005
§5. That is withdrawn. The prices are unchanged; what changed is that
they are not charged again.
The reason is what the namespace is for. A name that lapses is a name
somebody else can catch, and the whole pitch is that you can finally
hold the clean name instead of the hyphenated one you settled for. An
annual invoice with a drop date attached is the thing people are
trying to get away from, and selling it back to them undoes the pitch.
Names were already sold this way and only the comments said otherwise:
`moshpit_names` has never had an expiry column and nothing has ever
renewed a name. The PRD called it a yearly fee, the schema sold it
outright, and the doc comment described the PRD. It now describes the
code.
Endings really did have the machinery, but it never shipped -- nothing
in the app could open an ending checkout, so `quoteTld` and
`quoteRenewal` were unreachable and only the webhook settler was wired
up. No ending was ever charged a renewal and no row in the wild has an
expiry this takes away, which is why migration 016 is a plain drop
rather than a grandfathering policy: §21.8 asks for one before putting
endings INTO a lifecycle, and taking them back out of one nobody was
in needs no such thing.
- `expires_at` and `term_started_at` are dropped from `moshpit_tlds`,
columns and index. Gone rather than left NULL, because a nullable
expiry is an annual term waiting to be switched back on.
- `moshpit_tld_purchases` KEEPS `kind` and `years`. That is a financial
record of what was sold at the time, and a ledger is not something to
rewrite once the product changes.
- A 'renew' row can no longer be created, but one may still settle
late. It is honoured rather than refused -- the buyer keeps the
ending, which now keeps itself. Refusing it would take money for
nothing.
- `isExpired()` stays and always answers no, so callers asking a fair
question get a permanent answer instead of an import that fails.
The pit page leads with it, since it is the one claim here worth making
above the fold and the whole reason to prefer this over a registrar.
Note for whoever writes the next migration: nothing may follow the last
statement in a .sql file but whitespace. migrate.mjs splits on
semicolons and hands each piece to libSQL, and a trailing comment-only
piece comes back as `SQLITE_OK: not an error`, which is as unhelpful as
it sounds.
591 pass in apps/pwa.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWhPX5Uzd29whRg5WPAYM7
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

23 finding(s) in the 5 file(s) this pull request changes.

MEDIUM: 23

SeverityRuleLocation
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:74
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:88
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:114
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:301
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:305
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:350
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:574
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:755
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:757
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:816
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:862
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:932
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1035
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1058
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1080
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1279
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1429
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1578
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1584
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1631
MEDIUMsql-template-interpolationapps/pwa/src/moshpit.mjs:1680
MEDIUMjs-unescaped-html-sinkapps/pwa/src/routes/moshpit.mjs:2047
MEDIUMsql-template-interpolationapps/pwa/test/moshpit-terms.test.mjs:207
36 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 5 | **MEDIUM**: 25 | **LOW**: 6

Not introduced by this pull request. The full set is in the Security tab.

SeverityRuleLocation
HIGHjs-ssrf-outbound-requestapps/pwa/public/sw.js:45
HIGHtls-verification-disabledapps/pwa/src/lib/moshpit-gateway.mjs:299
HIGHsh-remote-script-executioninstall.sh:79
HIGHsh-remote-script-executioninstall.sh:83
HIGHtls-verification-disabledsrc/dns.mjs:766
MEDIUMjs-timing-unsafe-mac-compareapps/pwa/src/routes/settings-sync.mjs:192
MEDIUMjs-dynamic-code-executionapps/pwa/test/apikey-mask.test.mjs:129
MEDIUMsql-template-interpolationapps/pwa/test/credits-webhook-event-match.test.mjs:111
MEDIUMsql-template-interpolationapps/pwa/test/credits-webhook-event-match.test.mjs:131
MEDIUMsql-string-concatenationsrc/cli-schema.mjs:161
MEDIUMsql-string-concatenationsrc/cli-schema.mjs:503
MEDIUMsql-string-concatenationsrc/cli-schema.mjs:629
MEDIUMjs-uninitialized-buffersrc/pty.mjs:120
MEDIUMjs-timing-unsafe-mac-comparesrc/settings-sync.mjs:325
MEDIUMinsecure-temp-filetest/dns-disable-restore.test.mjs:93
MEDIUMinsecure-temp-filetest/dns-disable-restore.test.mjs:314
MEDIUMinsecure-temp-filetest/news.test.mjs:693
MEDIUMinsecure-temp-filetest/plugins.test.mjs:152
MEDIUMinsecure-temp-filetest/pty.test.mjs:28
MEDIUMinsecure-temp-filetest/pty.test.mjs:31

…and 16 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 136d13e into mainAug 28, 2026
6 checks passed
@ralyodioralyodio mentioned this pull request Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ralyodio