Skip to content

feat(projects): user-initiated slug registration + durable backend cache#66

Merged
agustinkassis merged 1 commit into
mainfrom
claude/lacrypta-dev-setup-884d7f
Jul 14, 2026
Merged

feat(projects): user-initiated slug registration + durable backend cache#66
agustinkassis merged 1 commit into
mainfrom
claude/lacrypta-dev-setup-884d7f

Conversation

@agustinkassis

@agustinkassis agustinkassis commented Jul 14, 2026

Copy link
Copy Markdown
Member

What & why

Adds a user-initiated project-slug registration flow and a backend-first durable cache that guarantees a registered project always resolves server-side.

This fixes the "Proyecto no encontrado" class of bug (e.g. /projects/pibot): a name-slug URL depended on the project's Nostr event being present in the broad submissions snapshot, and when a thinly-propagated event missed that scan the page degraded to a soft-404 with no server-side fallback. It also gives owners a real way to claim a clean URL instead of relying on the automatic name-derived registry sync.

How it works

Registry (slug → project)

  • New POST /api/projects/registry (+ GET ?slug= availability check). The owner signs a NIP-98 kind-27235 auth event; the backend verifies ownership against the project's own relay event, La-Crypta-signs the updated kind-30078 registry event, publishes it, and returns it so the client also republishes (belt-and-suspenders).
  • Changeable slugs, modeled append-only: a change appends a new entry that becomes canonical (latest-wins per id in buildRegistryState); the old slug stays an owner-locked redirect alias. This preserves the registry's monotonic-entries invariant, keeps old links alive, and drives the existing id→slug canonical redirect.
  • Shared getBackendSecretBytes / buildAndSignRegistryEvent are reused by the existing auto-sync path.

Durable "never not found" cache

  • New long-lived per-project Upstash copy (projectDurable), written on registration, on targeted refetch, and by the warm cron (registered projects only).
  • getProjectWithDurableFallback serves it when the live #d scan misses; the resolver uses it in the registered-slug + UUID branches.
  • POST /api/nostr/refresh gains a targeted per-project refetch branch (author-filtered, ~4.5s) instead of a full ~6s snapshot rescan.

Client

  • Owner-only "Registrar / Cambiar URL" card on the project page (ProjectSlugCard): prefilled slug, live availability, sign + republish, redirect to the new canonical slug. New projectRegistryClient helper.

Security hardening (from adversarial review)

  • Server-side ownership gate (project author must equal the auth-event pubkey) + recognized-author check against registry/snapshot, and an author-filtered project fetch — a stranger can't hijack a slug via a colliding #d event (#d is not owner-exclusive for NIP-33).
  • Append-only high-water-mark guard so a stale/partial/empty relay read can't sign a shrunken registry over the real one.
  • Curated-id rejection, reserved-slug + shape validation, per-author quota, and an Upstash write-lock serializing the registry read-modify-write across instances.
  • Durable-copy poisoning closed: writes are author-constrained and reads carry an expectedAuthor guard, so a poisoned copy is never served at a victim's canonical URL.
  • Consistent lowercased KV keys across all writers/readers.

Reviewer notes

  • The id→slug redirect is a client-side PPR redirect (the page resolves inside a <Suspense> under cacheComponents): a raw curl sees 200 + a correct <link rel="canonical">, while a real browser navigates to the slug.
  • LACRYPTA_NSEC is a Vercel Sensitive var and pulls back empty via vercel env pull; the registration backend is therefore unavailable in local prod-var runs (returns "Registro de URLs no disponible"). It works on the real deployment (key injected at runtime) and in the isolated dev setup (local relay + throwaway key).

Verification

tsc --noEmit clean; pnpm build clean (286 pages). Full flow exercised end-to-end in the isolated dev env (local relay + throwaway key): registered slugs via the real UI (sat-checkout-pos, nostr-id-demo) and API, confirmed /projects/<slug> renders and the id URL redirects to it, and availability is owner-aware.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Project owners can register and change a canonical, URL-friendly project address.
    • Added real-time slug availability checks, validation, and clear status messages.
    • Existing project links automatically resolve to the current canonical address.
  • Bug Fixes
    • Improved project loading reliability when relay data is delayed or temporarily unavailable.
    • Targeted project refreshes now preserve the complete project listing and use the freshest available data.
    • Added safeguards to prevent mismatched project data from being displayed.

Lets a project's author register (and later change) a unique URL slug for
their Nostr project, and guarantees a registered project always resolves
server-side — fixing the "Proyecto no encontrado" class of bug where a
thinly-propagated event missing from the broad snapshot stranded the page.

Registry (slug → project):
- New `POST /api/projects/registry` (+ `GET` availability check). The owner
  signs a NIP-98 (kind 27235) auth event; the backend verifies ownership
  against the project's own relay event, La-Crypta-signs the updated kind-30078
  registry event, publishes it, and returns it for the client to also
  republish (belt-and-suspenders).
- Changeable slugs modeled append-only: a change appends a new entry that
  becomes canonical (latest-wins per id in buildRegistryState); the old slug
  stays an owner-locked redirect alias. Preserves the monotonic-entries
  invariant, old links, and 308/canonical behaviour.
- Hardening: server-side ownership + recognized-author gate (registry/snapshot),
  author-filtered project fetch, curated-id rejection, reserved-slug + shape
  validation, per-author quota, and an Upstash write-lock serializing the
  read-modify-write. Shared getBackendSecretBytes/buildAndSignRegistryEvent
  reused by the auto-sync path.

Durable "never not found" cache:
- New long-lived per-project Upstash copy (projectDurable), written on
  registration, targeted refetch, and the warm cron (registered projects only).
- getProjectWithDurableFallback serves it when the live #d scan misses, with an
  expected-author guard so a poisoned copy is never served at a victim's
  canonical URL. `/api/nostr/refresh` gains a targeted per-project refetch
  branch (author-filtered) instead of a full 6s snapshot rescan.
- Consistent lowercased KV keys across all writers/readers.

Client:
- Owner-only "Registrar/Cambiar URL" card (ProjectSlugCard) on the project
  page with prefilled slug, live availability, sign + republish, and redirect
  to the new canonical slug. New projectRegistryClient helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lacrypta-dev Ready Ready Preview, Comment Jul 14, 2026 11:13pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@agustinkassis, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4f3de43-c9de-4b39-85b0-f397aab932c4

📥 Commits

Reviewing files that changed from the base of the PR and between 050d432 and 92cec5c.

📒 Files selected for processing (13)
  • app/api/cache/warm/route.ts
  • app/api/nostr/refresh/route.ts
  • app/api/projects/registry/route.ts
  • app/projects/[slug]/NostrProjectPageClient.tsx
  • components/ProjectDetailView.tsx
  • components/ProjectSlugCard.tsx
  • lib/nostrCache.ts
  • lib/nostrRevalidate.ts
  • lib/projectRegistry.ts
  • lib/projectRegistryClient.ts
  • lib/projectRegistryContract.ts
  • lib/projectResolver.ts
  • lib/upstashCache.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/lacrypta-dev-setup-884d7f

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit 252be97 into main Jul 14, 2026
2 of 3 checks passed
@agustinkassis
agustinkassis deleted the claude/lacrypta-dev-setup-884d7f branch July 14, 2026 23:20
Sign up for free to 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