Product Contract — Concept Registry
Reading guide
Design reference, not a beginner tutorial. This registry defines entities such as Seller, License, Entitlement, and PaymentEvent, including current records and proposed models.
For an introduction start with Concepts; use this document when checking identifiers and invariant responsibilities.
Status: PROPOSAL, grounded in the current implementation. "Identity" is the real ID/key format found in code; "Owner" is the party accountable for the record's truth; "States" are the values actually written by the code today; "Permission scope" is who can read/write it under current authorization. Concepts that exist only as design (e.g. a first-class Buyer entity) are marked as such rather than invented.
1. Registry
Seller
- Identity:
sellertableid=seller_{12hex}(auth_impl.rs:460); legacy seed valueseller_nougat. Slug:seller.slug→s-{…}on onboarding; public store path/store/{slug}. - Owner: DevPay (identity) + the seller themselves (profile/payout fields).
- States: no seller-level state machine today — the
sellerrow has nostatuscolumn. Verification state lives separately (verification.status, e.g.NOT_REGISTEREDon creation). - Permission scope:
users.role='seller'+seller_membershipsrow (role='owner') scoped viaseller_scope_db— DB re-check on every request. Seller sees only own products/orders/licenses/subscriptions/ settlements.adminsees all.
Buyer
- Identity: two partial identities, no unified entity. (a)
users.id = usr_{12hex}withrole='buyer'(signup default,auth_impl.rs:366); (b)orders.buyer_email/licenses.buyer_email— checkout requires only an email string, no account. - Owner: DevPay for
usersrows; the email identity is seller-visible data, not a platform credential. - States:
users.status=active|inactive(deactivate). Email-only buyers have no state. - Permission scope: buyers can call public/checkout endpoints and own
/meresources; no buyer dashboard exists. Buyer email is NOT proof of ownership (charter: unverified email must not claim purchases).
Product
- Identity:
products.id = prod_{12hex}(verify_impl.rsproduct_create); unique(seller_id, slug)enforced at create/update. - Owner: the owning seller; DevPay stores it.
- States:
status—active| non-active (checkout refusesPRODUCT_INACTIVE);type∈one_time|subscription|free|donation(FEProductType,fe/types/index.ts:25). - Permission scope: public read via
/public/stores/…(published sellers only); seller write own products;features_json/plans_json/license_json/entitlements_jsonare the product's policy payloads —features_jsonis the sole entitlement namespace source of truth.
License
- Identity:
licenses.id = lic_{12hex}(mock path,mk("lic")) or deterministiclic_{ord_id}(Toss finalize,toss_impl.rs:321—INSERT OR IGNOREmakes confirm/webhook retries idempotent).licenses.keyhas two observed formats: (a) purchase-issued (mock checkoutcheckout_impl.rs:165, Tosstoss_impl.rs:323):NG-{8hex}-{8hex}-{8hex}-{8hex}— 128-bit; (b) seller-issued viaPOST /licenses(endpoints_impl.rs:554-557, Axum parity):NG-{4hex}-{4hex}— 32-bit only. - KNOWN_LIMITATION: the 32-bit seller-issued key is brute-force feasible through the public, unauthenticated, un-rate-limited
POST /licenses/verify— ~4.3 × 10⁹ keyspace is enumerable at API request rates. Recommend migrating seller-issued keys to the 128-bit format and/or rate-limitingverify(currently neither exists). - Owner: DevPay issues; belongs to a
product_id(→ seller) and abuyer_email; optionalorder_id/subscription_idback-references. - States:
active→suspended(payment failure),revoked(cancel/refund),expired(period end /expires_atpassed);verify_status_errormaps toREVOKED|SUSPENDED|EXPIRED|INVALID_STATUS(verify_impl.rs).max_devices(default 3) slots enforced vialicense_bindingsatomic insert. - Permission scope: seller CRUD own licenses (
issue/patch/activations); publiclicenses/verifyis lookup-only (no slot writes); PoPactivateis key-holder + product-bound. - Two device models coexist: legacy
activations_json(display/records only — NOT authorization input) andlicense_bindings(jkt PoP slots — authorization).
Entitlement
- Identity: compact ES256 JWS; claims
iss(devpay-licWorker /ypSDK default),lic_id,product_id,features[],jkt_bound,cnf.jkt,iat,exp— TTL 120 s (license-crypto/lib.rs). SDK variant adds optionalaud,tenant_id. - Owner: DevPay (Worker
iss=devpay-lic) or the seller's license-server (BYOS,issconfigurable). Signed token is stateless; live license status re-queried on every use — revocation beats token validity. - States: none internally (exp-bound); effective states are derived: valid | expired | revoked-by-license-status.
- Permission scope: transport credential in
Authorization: Entitlement <jws>; never grants beyondproducts.features_json.
Feature
- Identity: a string in
products.features_json(e.g.premium_convert=FEATURE_PREMIUM_CONVERTinlicense-crypto/premium.rs). - Owner: seller (product definition); DevPay enforces membership.
- States: none (namespace entries, not entities).
- Permission scope: entitlement
features[]⊂ product features; unknown/empty →403 FORBIDDEN(has_feature,effective_featuresfail-closed). Onlypremium_convertis executable on the DevPay-hosted/licenses/executetoday — other features →400 unknown feature.
ProtectedOperation
- Identity:
(product_id, feature)pair — the server-side action gated by entitlement+capability. DevPay-hosted:POST /licenses/executewith{feature, input, cap_id}. BYOS: any seller route behindlicense.server.authorize/issueCapabilityorrequireLicense. - Owner: DevPay for hosted
execute(built-inpremium_convertonly); seller for BYOS operations. - States: per-call, no persistent object — success emits
UsageRecord. - Permission scope: entitlement + fresh DPoP proof + unconsumed capability bound to
lic_id+product_id+feature+jkt.
ExecutionCapability
- Identity:
execution_capabilities.cap_id = cap_{16hex}(new_id("cap")). - Owner: DevPay (Worker-issued) or seller store (BYOS
createCapability). - States:
consumed0→1 only; expiry viaexp(TTL 60 s,CAPABILITY_TTL_SEC); prune by cronprune_expired. - Permission scope: bound tuple
cap_id+lic_id+product_id+feature+jkt; consume is one atomicUPDATE … WHERE consumed=0— second use →409 CAPABILITY_CONSUMED+security_events(capability_reuse).
UsageRecord
- Identity:
license_usage.id = use_{16hex}; alsosecurity_events.id = sec_{16hex}for denials/replays/slot-overflow. - Owner: DevPay (written by hosted
execute/guard). - States: append-only fact rows (
lic_id,feature,at,meta_jsonincl.cap_id,product_id). - Permission scope: written server-side best-effort after execute (non-transactional — see trust-model §3 usage limits); no read endpoint today → seller/buyer visibility NOT_IMPLEMENTED.
PaymentEvent
- Identity:
payments.id = pay_{12hex};payments.payment_key= PGpaymentKey(toss). Money effects also land intransaction_ledger(led_{12hex},event_type∈payment_captured|payment_refunded| currency_grant|currency_revoke|license_issued|settlement) andoutbox(out_{12hex}, topic e.g.order.checkout). - Owner: DevPay ledger is authority; PG state reconciled via
checkout/confirm+webhooks/toss(signature-verified, ref-validated, refetched — never trusts the webhook body). - States:
payments.status=pending(Toss intent,toss_impl.rs:82) |paid(mock checkout / finalized Toss) |captured(feedback payments,endpoints_impl.rs:865) |refunded;orders.status∈paid|failed|canceled|refunded|pending|confirm_pending|partially_refunded. - Permission scope: seller reads own order/payment; public order status via
/public/orders/:id.provider∈mock|toss— mock blocked underPAYMENT_LIVE_ENABLED=true.
RefundEvent
- Identity:
refund_attempts.id = ra_{12hex}(mk_id("ra")). - Owner: DevPay record; initiated by owning seller (
POST /orders/:id/refund); PG executes the actual cancel (/v1/payments/{key}/cancel). - States:
attempted→succeeded|failed|needs_review(timeout/5xx — uncertain, requires resolution;order.refunded_krwsums onlysucceeded).GET /orders/:id/refund-attemptsis the audit view. - Permission scope: seller-only, ownership-scoped; partial refunds capped at remaining
amount_krw; full refund revokes license + fulfillment compensation in the same batch.
Subscription
- Identity:
subscriptions.id = sub_{12hex}(mock checkout) orsub_{ord_id}(toss path —toss_impl.rs:320, deterministic alongsidelic_{ord_id}); trackslicense_id,order_ids_json. - Owner: DevPay record; seller manages via
POST /subscriptions/:id/event {event: renew|fail|expire|cancel}. - States:
trialing(trial plan) |active|past_due|expired|canceled. Transitions: renew→active(+new paid order+ledger); fail→ past_due(+license suspended); expire→expired(+license expired); cancel→canceled(+license revoked).canceled/expiredare terminal for renew (409 SUB_ENDED). - Permission scope: seller events scoped to own products;
renewrequires non-live payment env (402 NOT_LIVE_PAYMENTunder live).
Tenant
- Identity: the seller boundary —
seller_idcolumn onprojects/products/orders+seller_memberships; in the SDK atenantId/tenant_idclaim concept exists (server.mjsexpectedTenantId, entitlementtenant_id). - Owner: DevPay (membership/scope enforcement via
seller_scope_db). - States: membership rows (
mem_{10hex},role='owner'). - Permission scope: seller token →
Some(seller_id)scope → WHERE clauses; admin →None(unscoped). Worker entitlement has no tenant_id claim — tenancy on the hosted path islic_id+product_id+jktbinding.
ProtectionSpecification
- Identity: JSON document,
version:1(spec/protection-spec.schema.json); bound downstream byspec_sha256inside the plan.project.idnames the subject. - Owner: the seller/agent drafting it; approval is a human/process act outside the tool (no signature field — deliberate).
- States:
draft→approved→implemented→verified. Drafts may carry literalREQUIRES_APPROVALmarkers;--apply-autoon a still-marked spec is refused (exit 2). - Permission scope: closed invariant registry (13 IDs, all
requires_server) — unregistered IDs make the spec invalid;auto_partition:"always"cannot override refusal screens.
ProtectionEvidence
- Identity: evidence record
schema_version:1(evidence/schema/evidence-schema.json) — one per(requirement_id, asset_id); artifacts sha256-pinned;commit_sha+worktree_dirtyrecorded. - Owner: independent verification principal (QA/Security lanes) — never the implementing party.
- States: verdicts
VERIFIED_WITHIN_SCOPE | FAILED | PARTIALLY_VERIFIED | NOT_VERIFIED | NOT_APPLICABLEper invariant (closed enum,evidence-schema.json:200); leg-levelPASS | FAIL | ATTACK_BLOCKED | ATTACK_REPRODUCED | LIMITATION_CONFIRMED | NOT_VERIFIED | NOT_APPLICABLE. - Permission scope: append-only evidence; validator enforces claim-gates (empty execution ≠ evidence; every asserted invariant needs a verdict; absolute claims rejected by claims-scan).
2. Cross-cutting identity formats (observed)
ord_{12hex} order · pay_{12hex} payment · lic_{12hex}/lic_{ord_id} license · sub_{12hex}/sub_{ord_id} subscription · set_{12hex} settlement (settlement_no = ST-YYYY-NNNN) · ord_{…}+order_no = NG-YYYYMMDD-NNNN (counter-atomic) · led/aud/out/act/gi/idem_{12hex} · cap/use/sec_{16hex} · ra_{12hex} refund attempt · usr_{12hex} user · seller_{12hex} seller · proj_{12hex} project · plr_{12hex} player · ff_{12hex} fulfillment · mem_{10hex} membership · sset_{10hex} settings · ver_{10hex}/ver_{seller_id} verification · lease_{8hex} outbox lease-owner token (not an entity — claim marker, checkout_impl.rs:285).
3. Invariants that must survive any redesign
products.features_jsonis the only entitlement namespace.- License status is re-read at every authorization — tokens never override revocation.
- Every consumption primitive is a single atomic statement (slots, jti, capability, counters, partial-refund cap).
- Missing row ≠ permission — every lookup failure denies (fail closed).
device_id/legacyactivations_jsonis never an authorization input;license_bindings(jkt) is.