@paysmith/framework-nextjs
v0.0.4
Published
Next.js App Router templates for the Paysmith $1 unlock demo — installed by the Paysmith CLI, unlocked only by verified signed events, removable via a hash manifest.
Downloads
723
Maintainers
Readme
@paysmith/framework-nextjs
The Next.js App Router templates for Paysmith's fixed $1 unlock demo, plus the manifest shapes
used to install and later cleanly remove them. This package is mostly consumed indirectly, through
paysmith init in @paysmith/cli — most users will never import it directly. It is documented here
for anyone building their own installer on top of it, or who wants to read the generated source
before running the CLI.
The demo it generates funds a sandbox with a deterministic $100.00 fake balance, expires 72 hours
after activation, and unlocks its one entitlement only after its own server verifies a signed
Paysmith event — never from a checkout response.
Those signed events reach the app two ways, and the difference is transport only. Paysmith pushes
them to app/api/paysmith/webhook/route.ts, and — because a control plane that is not on your
machine cannot POST to an app on localhost — the app also pulls the same envelopes from the
event feed while the demo page polls. Both run the identical verify → deduplicate → grant pipeline in
lib/paysmith/process-event.ts, against the same pinned key, and processing is deduplicated by
event_id, so an event that arrives both ways unlocks exactly once.
Install
pnpm add @paysmith/framework-nextjs
# or
npm i @paysmith/framework-nextjsWhat it installs
Eleven files, all newly created — this manifest never edits an existing file:
| Target path | Purpose |
| --- | --- |
| app/paysmith-demo/page.tsx | The demo page: Pay $1 to unlock, plus a scenario picker for declined/delayed/duplicate-webhook/refund-after-success. |
| app/paysmith-demo/paysmith-demo.module.css | Its styles. |
| app/api/paysmith/checkout/route.ts | Creates a sandbox payment intent for the fixed $1 price. Never reports success itself. |
| app/api/paysmith/checkout/confirm/route.ts | Forwards the developer's chosen sandbox outcome scenario. Also never reports success itself. |
| app/api/paysmith/webhook/route.ts | Push transport: reads the raw delivery bytes and hands them to the pipeline, then maps the outcome onto the status codes Paysmith's retry logic expects. |
| app/api/paysmith/entitlement/route.ts | Reads the local entitlement projection for the current browser session — after a best-effort pull of any signed events the app has not seen. |
| app/api/paysmith/receipt/route.ts | Returns the receipt earned by the current session's entitlement, never an arbitrary id. |
| lib/paysmith/server.ts | The singleton @paysmith/sdk/server client, plus the pinned public_key_id every event is verified against. |
| lib/paysmith/process-event.ts | The only code that grants or revokes demo.premium: verify against the pinned key → deduplicate by event_id → project. Shared by both transports. |
| lib/paysmith/entitlements.ts | File-backed demo storage (below). |
| .env.example | Documents the four environment variables the templates read. |
The file-backed demo entitlement store
lib/paysmith/entitlements.ts persists everything the demo needs to run without a database into a
single JSON file, .paysmith/entitlements.json:
- the entitlement projection (
locked/unlocked/revoked) that a verified event grants or revokes — never anything a client claims; - the ids of events already processed, adapting to
@paysmith/webhook'sEventDeduperStoreviacreateEntitlementsDeduperStore(), so a re-delivered — or re-pulled — event can never grant or revoke twice; - the sandbox's signing public key, fetched once and cached;
- the pull cursor (
lastPulledEventId), so a restarted dev server does not re-fetch every event the sandbox ever emitted. It is a transport cursor only: it advances just past events that were actually processed, and losing it costs redundant work rather than a wrong entitlement.
Writes go to a sibling temp file and are published with a single rename, and every read-modify-
write is serialized through an in-process queue, so concurrent deliveries (the duplicate_webhook
demo scenario delivers the same event twice on purpose, and a pull can race a push) can't race each
other.
This store is intentionally minimal — a real integration should replace it with real persistence;
the sandbox itself, not this file, remains the source of truth for what actually happened.
Environment variables (.env.example)
| Variable | Required | Purpose |
| --- | --- | --- |
| PAYSMITH_API_BASE | yes | Base URL of the Paysmith sandbox API — a loopback origin when you run the control plane yourself, or a remote https endpoint when you use a hosted one (the app only ever calls out to it, so pull mode works from localhost). Must be https unless loopback. |
| PAYSMITH_SANDBOX_TOKEN | yes | The scoped token issued for the sandbox. |
| PAYSMITH_PUBLIC_KEY_ID | yes | The public_key_id this sandbox was activated with. Signature verification is pinned to it, on both transports. |
| PAYSMITH_WEBHOOK_URL | no | Loopback override. The sandbox only delivers webhooks to loopback origins; set this only when your app is hosted on a public domain while sharing a machine with the control plane (e.g. http://127.0.0.1:3000/api/paysmith/webhook). Leave unset for local dev, and for a remote control plane — that one cannot reach your app at all, which is what pull mode is for. |
All four are server-only: never prefix any of them with NEXT_PUBLIC_.
Quick start (programmatic use)
import {
TEMPLATE_MANIFEST,
TEMPLATE_RUNTIME_DEPENDENCIES,
buildGeneratedManifest,
} from "@paysmith/framework-nextjs";
TEMPLATE_MANIFEST[0];
// { sourcePath: "templates/app/paysmith-demo/page.tsx", targetPath: "app/paysmith-demo/page.tsx", kind: "create" }
TEMPLATE_RUNTIME_DEPENDENCIES;
// ["@paysmith/contracts", "@paysmith/sdk", "@paysmith/webhook"]
// (next/react are deliberately excluded — a project this generator targets already has them.)
const manifest = buildGeneratedManifest([
{ path: "app/paysmith-demo/page.tsx", sha256: "sha256:6b2f...c91a" },
// ...one entry per file actually written, hashed by the caller
]);
// { schema: "paysmith.generated/v1", created_at: "2026-08-08T12:00:00Z", files: [...] }buildGeneratedManifest only shapes the paysmith.generated.json document — it never touches the
filesystem or hashes anything itself. The caller (the CLI) writes the files, computes each
sha256:<hex> digest, and passes the results in. That manifest is what makes paysmith remove
possible: it deletes only files whose hash still matches what was generated, and preserves anything
you've since edited.
API
TEMPLATE_MANIFEST: readonly TemplateManifestEntry[]—{ sourcePath, targetPath, kind: "create" }for every file above;sourcePathis relative to this package's root,targetPathto the target app's root.TEMPLATE_RUNTIME_DEPENDENCIES: readonly TemplateRuntimeDependency[]— the non-framework packages the generated files import by name (@paysmith/contracts,@paysmith/sdk,@paysmith/webhook).buildGeneratedManifest(files: readonly GeneratedManifestFile[]): GeneratedManifest— wraps already-hashed files in thepaysmith.generated/v1envelope (GENERATED_MANIFEST_SCHEMA).FRAMEWORK_NEXTJS_PACKAGE_NAME— the package's own name, as a literal.
How it fits
This package is the product and its surrounding UI, made concrete: a demo page that starts a payment intent, one verification pipeline that is the only place a signed event becomes an entitlement, and a receipt route that surfaces the resulting receipt. All five primitives, wired end to end, in files you can read.
License
MIT
