Architecture.
One page, one pass. Enough to load the product in your head without reading the source.
Casset is a Next.js App Router app with a Postgres database (Prisma), Vercel Blob for audio + artwork, Stripe for payments, Redis for presence / rate limiting, and a canonical release layer for manifests, provenance, permissions, lineage, and quiet Base anchoring. The surface you see at casset.fm is a mix of static docs/marketing pages, dynamic Profile Worlds, creator Studio surfaces, Visual Studio/runtime layers, internal release dossier tooling, and API routes that connect them.
For the living route classification, source-of-truth ownership, peripheral system posture, and code verification commands, read system reality. This page explains the architecture; system reality keeps the claims tied to current source files.
At a glance
HUMAN EXPERIENCE
┌──────────────────────────────────────────────────────────────────────┐
│ Browser / PWA │
│ Profile Worlds · Hook Objects · Listening Rooms · Studio · Dossier │
└──────────────▲───────────────────────┬───────────────────────────────┘
│ HTML / RSC / JSON │ SSE: activity, presence, room
│ │
┌──────────────┴───────────────────────┴───────────────────────────────┐
│ Next.js App Router │
│ app/[slug] · app/studio · app/internal · app/docs · app/api/* │
└──────┬──────────────────┬──────────────────────┬────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌─────────────────────┐ ┌─────────────────────────┐
│ Profile data │ │ Canonical release │ │ Agent-readable layer │
│ Artist/Track │ │ ReleaseVersion │ │ /api/agents/releases/* │
│ theme/runtime│ │ Manifest/Provenance │ │ manifest · dna · policy │
└──────┬───────┘ │ Permissions/Lineage │ │ access · license · 402 │
│ └──────────┬──────────┘ └───────────┬─────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌─────────────────────┐ ┌─────────────────────────┐
│ Vercel Blob │ │ Postgres / Prisma │ │ MPP access rail │
│ audio/art │ │ canonical source │ │ challenge · credential │
│ artifacts │ │ manifests + jobs │ │ receipt · provenance │
└──────────────┘ └──────────┬──────────┘ └─────────────────────────┘
│
▼
┌─────────────────────┐
│ Optional Base proof │
│ ReleaseAnchor jobs │
│ tx receipt/explorer │
└─────────────────────┘
Supporting rails: Stripe Connect/Apple Pay for human commerce, Redis for
presence/rate limits/job acceleration, Vercel for edge delivery.Data model
schema.prisma defines the application's identity, playback, social, commerce, and release infrastructure models. You rarely need to know all of them: most public flows touch a small ring of Profile World models, while the newer release layer records canonical manifests, contributors, permissions, provenance, and lineage.
The core ring
User— an account. Owns auth providers (OAuthAccount), optional Profile World/casset rows throughArtist, purchase history, co-casset memberships, visual assets, andFollowedges.Artist— the current Profile World/casset record. Holds the public handle, cover, avatar,themeJson, footer theme, price, privacy/publish state, Stripe Connect account id, and tracks.Track— a song. Points at the audio blob, storespreviewStartSec(the hook), duration, optional per-track rewards, sort order, lyrics, and rendered visual artifacts.Purchase— the ledger entry that says "user X unlocked casset Y for $Z on date D." Entitlement checks resolve here.
Artist.themeJson is the flexible profile identity payload: stored color tokens, custom swatch source, profile details, cassetBackgroundSource, profilePatternId, and the mutually exclusive aiUsedProfile / noAiUsedProfile badge flags.
Community + activity
CassetActivity+ActivityEvent— the feed that surfaces "@connor collected Lil Durden’s casset" to followers.CassetComment,HookComment,HookCommentLike,HookLike— comment + like threads on both the casset page (Side B / Listening Room) and per-hook discussion.Emoji,TrackReaction— custom per-artist emoji fans can purchase and drop as reactions.
Release quests, campaigns, and rewards
Campaign/CampaignParticipant/CampaignSubmission— a "drop" is a time-boxed release-quest system where fans submit hook videos and winners split a prize pool.SubmissionMetrics+SubmissionScore— scoring pipeline that ranks entries. Integrity scoring lives inlib/scoring/.Bounty,HookShare,HookVideoSubmission— per-hook rewards attached directly to a track (no campaign needed).
Visual identity + co-cassets
VisualPack,VisualAsset,HookVisualAttachment,VisualGenerationJob— reusable visual-world assets, generated outputs, hook attachments, and render/source metadata.CoCasset,CoCassetMember,CoCassetTrack,CoCassetRoomMessage— shared music identity objects with members, tracks, reactions, cover state, and room messages.
Intelligence + strategy
ArtistDropPlan,DropIntelligenceSnapshot,DropStrategyRecommendation— the advisor layer that suggests release-quest configuration based on historic engagement.PromoterProfile,PromoterReputation,PromoterSubscription— optional campaign/release-quest reputation graph. This is not the core product category.
Canonical release layer
Release/ReleaseVersion— the canonical release definition and immutable version snapshots behind the public music experience.ReleaseManifest— deterministic JSON snapshot with schema version, canonical hash, tracks, audiovisual refs, contributors, splits, and permission state.ReleaseAnchor— optional Base-backed proof record for a manifest hash. Base is infrastructure, not the product surface.Contributor,ReleaseContributor, andSplit— film-credit-style contributor graph and settlement references.RightsScope,PermissionPolicy, andAgentAccessPolicy— release-authored rules for remix, stems, sync, visual reuse, AI training, AI generation, derivatives, and commercial usage.ProvenanceEventandDerivativeLink— append-only release history and source/derivative lineage.
Accounting
CreditLedger— append-only credits + debits so any balance can be reconstructed (never mutate rows; insert reversing entries).CampaignPayout,PerformancePayout— payout records with statuses mirrored from Stripe Connect transfers.
Request lifecycle
Here's what happens when someone taps casset.fm/lildurden from an iMessage link.
- Edge. Vercel routes the request to the nearest region.
middleware.tsdoes auth cookie validation + minor rewrites (e.g. collapsing/share/[slug]→/[slug]). - RSC.
app/[slug]/page.tsxruns on the server, callsgetData(slug)(cached per-request viaReact.cache), and returns the HTML shell with artist data baked in.generateMetadataruns in parallel so crawlers get OG tags on the first byte. - Hydrate. The client bundle takes over. The dynamic theme hook (
useDynamicTheme) samples the cover image and computes UI tokens; the media footer boots audio;next/imagestreams artwork with the blurhash placeholder. - Playback. Tap play → the audio element points at
/api/audio/[trackId]with a signed token. Server verifies entitlement vialib/audio-access.tsand returns a byte range covering the hook window (or the full track for unlocked users). - Realtime. The client opens an SSE connection to
/api/casset/[slug]/sidebfor comment + activity updates, which stream in without a refresh.
Release manifest lifecycle
The release layer runs beneath the public cinematic surface. A listener experiences a Profile World; an owner or agent can resolve the canonical release definition.
Artist/Profile data
-> Release + draft ReleaseVersion
-> freeze/publish ReleaseVersion
-> deterministic ReleaseManifest snapshot
-> canonical JSON + canonical hash + signature metadata
-> append ProvenanceEvent chain
-> expose agent-readable manifest / DNA / permissions / lineage
Optional proof branch:
-> queue ReleaseAnchor (idempotency key = chain + version + hash)
-> BASE_ANCHOR job claims QUEUED -> ANCHORING
-> submit manifest-hash calldata to Base Sepolia by default
-> normalize tx receipt + explorer refs
-> ANCHOR_VERIFY job confirms hash/proof integrity
-> append ANCHOR_RECORDED or ANCHOR_FAILED provenance
Permissioned access branch:
-> PermissionPolicy / RightsScope / AgentAccessPolicy
-> ALLOW | DENY | CONTACT_OWNER | REQUIRE_LICENSE
-> REQUIRE_LICENSE returns HTTP 402 + MPP challenge
-> verified MPP credential returns Payment-Receipt + resource
-> ACCESS_GRANTED / LICENSE_RECEIPT_RECORDED provenance eventslib/releases/manifest-builder.tsbuilds stable manifest payloads and hashes.lib/releases/manifest-runtime.tscreates idempotent manifest snapshots from release versions.lib/releases/permissions.tsevaluates rights scopes and agent policies.lib/releases/provenance.tsappends hashed provenance events.lib/base/release-anchor.tsqueues, retries, and records Base anchors for manifest hashes.lib/base/sepolia-anchor.tssubmits zero-value calldata on Base Sepolia by default and blocks mainnet unless explicitly enabled.lib/releases/agent-api.tsserializes agent-visible manifest, DNA, provenance, permission, and lineage responses.lib/releases/access/mpp-release-access.tsturns aREQUIRE_LICENSEpermission decision into an MPP challenge, verifies credentials, returns receipt headers, and records access lifecycle events as provenance.
Agent access and MPP
MPP is not a second release model. It is the payment/access negotiation rail after Casset has already resolved the canonical release and evaluated permission policy. The source of truth remains ReleaseManifest, PermissionPolicy, AgentAccessPolicy, and ProvenanceEvent.
Agent request
-> GET /api/agents/releases/<releaseId>/manifest | dna | provenance | lineage
-> POST /api/agents/releases/<releaseId>/permission-check
-> decision:
ALLOW return resource
DENY deterministic denial
CONTACT_OWNER contact route, no payment challenge
REQUIRE_LICENSE HTTP 402 + WWW-Authenticate: MPP challenge
-> agent presents MPP credential
-> verify challenge id, signature, expiry, resource binding, request hash
-> return Payment-Receipt + manifest/license/stems/dna resource
-> append provenance: ACCESS_CHALLENGE_ISSUED,
ACCESS_CREDENTIAL_FAILED,
ACCESS_GRANTED,
LICENSE_RECEIPT_RECORDEDlib/mpp/challenge.tscreates signed or demo MPP challenges bound to release id, release version id, canonical hash, scope, resource type, request digest, and settlement split refs./api/agents/releases/[releaseId]/licenseand/api/agents/releases/[releaseId]/accessreturn 402 problem responses when payment/licensing is required, then return aPayment-Receiptheader after verification./openapi.jsonis generated fromlib/mpp/discovery.tsso machine clients can discover release manifest, permission, license, and access routes without reading the human UI.
Base proof role
Base remains quiet proof infrastructure. It anchors release manifest hashes and receipt/proof references; it does not issue creator coins, replace MPP, or become the public mental model for a listener.
ReleaseManifest.canonicalHash
-> ReleaseAnchor(status: QUEUED, chain: base-sepolia, idempotencyKey)
-> app/api/cron/releases/process-jobs
-> claimNextReleaseAnchorJob()
-> submitReleaseAnchorToBase()
- default chain: base-sepolia
- mainnet requires BASE_ANCHOR_ALLOW_MAINNET=1
- calldata: casset.release-anchor.v1:<versionId>:<hash>
- zero-value transaction
-> normalizeBaseReceipt()
-> record txHash, blockNumber, contractAddress, explorer refs
-> append provenance + schedule anchor verificationThe audio pipeline
Upload (Studio) Storage Stream (listener)
────────────────── ────────────── ────────────────────
pick file Vercel Blob /api/audio/<trackId>
│ ▲ │
▼ │ ▼
magic-byte put(sha256-keyed) audio-access.ts
validation with content-type │
│ │ ┌────────┴────────┐
▼ │ ▼ ▼
sha256 dedup │ entitled? hook only
│ │ sign+302 byte-range proxy
▼ │ (30s window)
Track row + peaks │
(waveform pre-compute) │The key invariant: the raw audio URL never leaves the server. Even unlocked listeners get a signed redirect that expires in ~1 minute, and unpurchased listeners get a proxied byte range limited to the 30s hook window — computed from PREVIEW_START_SEC and AUDIO_PROXY_WINDOW_SEC in lib/audio-access.ts.
Full deep-dive: audio pipeline.
The commerce pipeline
Fan taps Collect ──► Apple Pay sheet (Stripe Payment Request)
│
▼
POST /api/checkout/payment-intent
│
▼
Stripe creates PaymentIntent (Destination charge, app fee)
│
▼
Client confirms ──► webhook: payment_intent.succeeded
│
▼
grant Purchase + Credit row
│
▼
SSE push to /sideb ("@fan collected …")
│
▼
Referral attribution (if tracked cookie)
│
▼
Stripe Transfer ─► artist's connected accountThe webhook is the source of truth, not the client. Even if the tab crashes mid-confirm, once Stripe fires payment_intent.succeeded, we grant entitlement and emit the activity. Deep-dive: commerce.
Realtime (SSE over HTTP/2)
Casset uses plain text/event-stream SSE rather than WebSockets — cheaper on Vercel's serverless model, strictly one-way (client never writes back), and trivially survives middleboxes that break WS.
/api/casset/[slug]/sideb— comment stream + activity feed for a casset page. Fans get a live count + avatar of who's browsing./api/activity/stream— the global activity stream that powers the/feedpage and notification badges.- Presence is stored in Redis with a 30s TTL, refreshed by a heartbeat from each connected client. Falling off the list happens naturally.
Rendering strategy
The mental model: static for marketing, dynamic for identity, client for playback.
- Static pre-rendered (
○innext build):/,/features,/about,/docs/*,/login,/signup. No database round-trip on cold load. - Dynamic SSR (
ƒ):/[slug],/preview/[slug],/u/[username],/studio/casset/[id]. Each renders with live artist data and returns owner-only fields only when the session cookie matches. - Client islands: the media footer, the preview scrubber, the visualizer, the TikTok export modal, and all of
/studio. These are hydrated behindnext/dynamicwithssr: falseso the server-rendered shell ships fast and the interactive bits stream in after.
Open-graph images
Every casset has a dynamic OG card rendered by app/[slug]/opengraph-image.tsx via next/og. The card uses the artist's pfp + cover + name with AeonikPro burned in so chat apps see a branded 1200×630 preview even before the artist claims custom artwork.
The boring but important stuff
- Migrations. Production runs
prisma migrate deployonly. Local usesprisma migrate dev. Seedocs/MIGRATION_POLICY.md. - Env contract.
GET /api/env-checklists the envs each route depends on;pnpm devwill bail if one is missing. - Testing.
vitestfor unit + route tests. Tests that touch Stripe or Prisma run against an isolated sandbox, never shared credentials. - Observability. Sentry for errors; Vercel Analytics for traffic; Redis for short-term metrics. No heavy APM layer.