@volter/twin-svix
v0.1.37
Published
Local Svix twin for the webhook-delivery-as-a-service surface: Application/Endpoint/Message CRUD, a deterministic per-endpoint Message Attempt fan-out + delivery-state-progression machine (pending->sending->success, or a configured-failure retry counter s
Readme
@volter/twin-svix
Local Svix twin for the webhook-delivery-as-a-service surface: Application/Endpoint/Message
CRUD, a REAL kernel-persisted per-endpoint Message Attempt fan-out + delivery-state-progression
machine — pending(1) -> sending(3) -> success(0), or a configured-failure retry counter
sending -> fail(2) -> a NEW retry attempt row -> ... -> terminal fail(2) — Event Type CRUD,
and the REAL Standard-Webhooks signature scheme (svix-id/svix-timestamp/svix-signature,
whsec_ secret, HMAC-SHA256) — PORTED, not written fresh, from
packages/twin/clerk/src/clerk-events.ts (clerk/resend/vital
already fake this exact scheme ad hoc for their own outbound webhooks). Built on the shared
@volter/world-core kernel. API-first vendor (no product UI to mirror — the Svix dashboard is a
delivery-inspection/config view, not an agent navigation target).
bun packages/twin/svix/src/cli.tsSingle host, nested path-primary routing
Unlike qstash's flat single-segment dispatch, every real Svix call hits ONE host —
api.svix.com/api/v1/... — with endpoint/message/attempt resources NESTED under
/app/{app_id}/... (see svix-twin.ts header): app | endpoint | msg | attempt |
event-type. {app_id} (and {endpoint_id}) may be the real id OR the caller-supplied uid
(GROUNDED: every real path-param doc-comment reads "The Application's ID or UID").
Authorization: Bearer <token> is accepted but not enforced (auth.token_401_parity is the
honest todo this leaves).
The per-endpoint Message Attempt fan-out + delivery machine (the signature strength)
svix-runtime.ts is written FRESH for this pack (modeled on the upstash/qstash lane's semantics/delivery.ts's
advanceMessageDelivery poll-fold, itself modeled on inngest-runtime.ts's advanceRun). This
twin does not make a real outbound HTTP request to any endpoint URL — there is no network
egress here. Given a created message, delivery-attempt progression is a deterministic,
kernel-persisted poll-fold:
message_attempts.fanout—message.createimmediately creates ONE seq-0pending(1) message_attempt row PER endpoint whosefilterTypes/channelsmatch the message'seventType/channels(unfiltered = matches everything; disabled endpoints never match —message_attempts.filtered_fanoutproves the real filter PHYSICS, not just config-acceptance).message_attempts.state_progression(prime) —GET /api/v1/app/{app}/attempt/msg/{msg}is the DISCLOSED poll-fold ADVANCE trigger (a poll-fold, advancing on each read): each call advances EVERY open attempt for that message by ONE step (pending->sending->success, terminal + idempotent from there) before returning the list. The real vendor's GET is a pure read with no side effects — this twin's is not, disclosed.GET .../attempt/endpoint/{ep}andGET .../msg/{msg}/attempt/{attempt_id}are PURE reads (no advance) — the honest way to observe a freshpendingrow without driving it forward.delivery.retry_count_exact(prime) — an endpoint created with the twin-onlysvix-twin-fail-attempts:<N>header (or a URL containing the sentinel substringfail., using the GROUNDED default cap) advancessending->fail(2), and — while its seq/retryCount is still belowN— a GENUINE NEWmessage_attemptrow (seq+1) is createdpending. This is the REAL vendor's own shape (GROUNDED:MessageAttemptOutcarries its OWNid/timestampper row — each retry really is a new record, not a mutated counter on one row). Once the highest-seq row'sretryCountreachesN, no further row is created — that row is the terminalfail.
Disclosed deviation: retryCount on the message_attempt view
The real MessageAttemptOut type (GROUNDED from api.svix.com's own OpenAPI document) has no
retryCount field. This twin ADDS it, additively, on top of every real field in
GET .../attempt/... responses, purely for delivery-progress observability (its value equals the
row's own seq — the number of retries that had already happened before this row was created).
Every OTHER field on the attempt view is the real, GROUNDED shape (id, msgId, endpointId,
status, responseStatusCode, responseDurationMs, url, timestamp; statusText is this
twin's own status->text label, matching the real enum's own names).
GROUNDED retry default: 8 total attempts (7 retries), not the build spec's "~5" guess
docs.svix.com/retries (read-only, 2026-07-09): the real schedule is immediately, then
5s/5m/30m/2h/5h/10h/10h — 8 total delivery attempts (1 initial + 7 retries) before a message is
marked permanently failed. svix-runtime.ts's DEFAULT_MAX_RETRIES = 7 uses this confirmed value
when a configured-failure endpoint doesn't explicitly override the count via
svix-twin-fail-attempts:<N>.
Standard-Webhooks signing — PORTED, not fresh (the archetype's defining feature)
svix-signing.ts PORTS clerk-events.ts's computeSvixSignature/verifyWebhook byte-for-byte
(cross-pack import is forbidden — architecture.test.ts enforces it — so this is a copy, not an
import): svix-id/svix-timestamp/svix-signature: v1,<base64 HMAC-SHA256(key,
"${id}.${timestamp}.${payload}")>, secret whsec_<base64>, HMAC key = the base64-decoded portion.
GROUNDED byte-for-byte against docs.svix.com/receiving/verifying-payloads/how-manual AND the
installed [email protected] package's own src/webhook.ts (a thin wrapper around the
standardwebhooks package). Cross-SDK validated live, before svix-sdk.integration.test.ts was
written: a signature built by buildSignedSvixDelivery is ACCEPTED by the real
new Webhook(secret).verify() and correctly REJECTED when the payload is tampered — not just
self-consistent, genuinely interoperable with the real vendor's own verification code (the same
class clerk/resend rely on for their own ad hoc scheme). Pure crypto, svix-signing.ts NEVER
imports the twin handler — the three signing.* capabilities are legitimately mutation-test
ALLOW-listed (svix.signing.sign_verify, svix.signing.reject_tampered,
svix.signing.reject_body_tamper).
Why "centralizes" doesn't mean an in-place dedup of clerk/resend/vital
The roadmap candidate's own framing ("several existing packs, e.g. clerk, already fake
svix-signing ad hoc — a real twin centralizes it") means this pack becomes the ONE canonical
reference implementation of the Standard-Webhooks scheme going forward — cross-pack imports stay
forbidden (architecture.test.ts), so clerk-events.ts/resend-events.ts/vital-events.ts keep
their own independent copies, untouched (DO-NOT-TOUCH). An actual in-place dedup of those three
packs onto this one is a disclosed future refactor, not part of this build.
The kernel type/id/updatedAt gotcha — found in ALL 3 timestamped resources
Per the build spec's mandate to audit EVERY resource's top-level fields, all 5 resources
(application, endpoint, message, message_attempt, event_type) were checked against the kernel's
reserved META set (type/id/updatedAt, packages/world-core/src/actions.ts).
Collisions found in application/endpoint/event_type: the real id field on
application/endpoint (GROUNDED — ApplicationOut.id/EndpointOut.id) collides with the reserved
set, resolved by storing it as app_id/endpoint_id and re-attaching the bare id ONLY at the
view layer; all three resources' real updatedAt field (GROUNDED, every *Out type) collides too,
resolved by storing it as app_updated_at/endpoint_updated_at/event_type_updated_at and
remapping to updatedAt ONLY in svix-twin.ts's view functions. message/message_attempt also
have a real id field (message_id/attempt_id, same treatment). event_type has NO id
field at all — the real vendor uses name itself as the resource's own identifier (GROUNDED,
EventTypeOut has no id property) — name is a safe field name, not reserved. eventType on
message is SAFE (≠ the literal reserved key type) — stored as event_type. status on
message_attempt is SAFE (numeric, not type/id/updatedAt). No resource has a top-level
literal type field.
Coverage
This is a v1 slice of the Svix API, not the full surface. Modeled done (45): application
create/create-with-uid/get/get-by-uid/list/delete/get-unknown-404, endpoint
create/create-with-filter-types/create-with-channels/get/list/update/delete/get-secret/
rotate-secret, message create/create-requires-fields/get/list/id-format/isolation, the delivery
machine (created-pending, state progression, fanout, filtered fanout (real physics),
list-by-message, list-by-endpoint, single-attempt get, success-terminal), the exact retry
counter + retry-then-fail, event-type create/list/get/delete, Standard-Webhooks sign/verify/
tamper/body-tamper, read-only/unmodeled-route safety, kernel-persisted state, conformance
snapshot, and a handle-driven connector pull (idempotent, folds applications+messages+attempts).
Left as todo (29, honest gaps): endpoint custom headers/transformation/rate-limit/
disable-toggle/recover/bulk-replay/stats/uid-lookup, attempt resend/status-filter/cursor-pagination/
Canceled-status/list-attempted-destinations/list-attempted-messages, message
expunge-content/with-content-query/cursor-pagination/tags/deliverAt, event-type update/
import-openapi/schema-validation, application pagination/rate-limit, auth token-401-parity, the
full error-code taxonomy, connector push + endpoint-pull, and fixture seeding.
Deviation from the build spec's approximate ~28-30-done headline (disclosed, mirrors
the qstash twin's own precedent of trusting confirmed grounding over an approximate
summary count): this build's api.svix.com OpenAPI fetch fully GROUNDED several items the build
spec marked ⚠ doc-UNVERIFIED (delete status codes, the list envelope shape, message-create's 202
status, endpoint-update's 200) — kept done rather than pre-emptively demoted, since every one is
genuinely verify-proven. total (77) comfortably clears the >=50 floor with done>0.
Planned (todo): deliver the signed message to the endpoint URL over HTTP
(svix.delivery.http_delivery) — today attempts advance through a deterministic kernel poll-fold
and no request leaves the process, with the backoff schedule collapsed to zero so a suite is
reproducible. Per-endpoint rateLimit/throttleRate enforcement is filed as
svix.endpoints.rate_limit.
See src/svix-capabilities.ts for the full manifest (the real vendor surface is the denominator —
coverage is honest and partial until the twin reaches it).
Grounding beyond docs
This build read-only npm pack svix'd the ACTUALLY-INSTALLED 1.96.1 package's own compiled
source (not just docs.svix.com prose) AND fetched api.svix.com's own published OpenAPI
document (read-only, unauthenticated) — the strongest grounding available for a REST surface —
confirming several facts the build spec had marked ⚠ doc-UNVERIFIED:
MessageStatusenum is EXACTLYSuccess=0, Pending=1, Fail=2, Sending=3, Canceled=4(src/models/messageStatus.ts) — the build spec's own 0/1/2/3 numbering was already correct;Canceled=4is new confirmed information, lefttodo.- ID formats are KSUID-shaped and GROUNDED byte-for-byte from the OpenAPI document's own
pattern/examplefields:app_<27 alnum>,ep_<27 alnum>,msg_<27 alnum>,atmpt_<27 alnum>(NOT a genericattempt_prefix — a genuine correction).event_typehas noidfield at all (nameis the identifier). - Status codes, confirmed per-route from the OpenAPI document's own
responsesmap: application create 201, application delete 204, endpoint create 201, endpoint delete 204, endpoint secret-rotate 204 ("no content"), message create 202 Accepted (not 200/201 — a genuine correction), message-attempt list-by-message 200, resend 202, event-type create 201, event-type delete 204. - The error envelope is GROUNDED byte-for-byte:
HttpErrorOut = {code: string, detail: string}(both required) for generic errors;HTTPValidationError = {detail: ValidationError[]},ValidationError = {loc, msg, type}for a 422 structural-validation failure (real FastAPI shape). - The list envelope is GROUNDED byte-for-byte:
{data, iterator, prevIterator, done}(ListResponseApplicationOutand siblings) — this twin always returns one full page (iterator/prevIterator: null,done: true); real cursor pagination is the honesttodo. MessageIn.required = [eventType, payload];EventTypeIn.required = [description, name]— GROUNDED, confirms the exact 422-triggering fields.- The retry schedule is GROUNDED (
docs.svix.com/retries): 8 total attempts (1 initial + 7 retries) — corrects the build spec's own "~5" guess;DEFAULT_MAX_RETRIES = 7. - The highest-risk item (build spec §7.2/§13 risk-1) — whether
new Svix(token, {serverUrl})routes.application.create()/.message.create()to a local server — was verified LIVE (a throwawayBun.serveserver) BEFOREsvix-sdk.integration.test.tswas written: confirmed, including the exact real request paths/headers. The realWebhook(secret).verify()class was ALSO verified live againstsvix-signing.ts's own ported output before that integration test was written — seesvix-sdk.integration.test.ts's header.
endpoint.secret's exact rotate grace-period behavior (real: previous secret valid for
gracePeriodSeconds, default 24h) is a disclosed SIMPLIFICATION — this twin invalidates the old
secret IMMEDIATELY on rotate, not after a grace window (see svix-signing.ts's
deriveSvixEndpointSecret header). The exact code string values in the {code,detail} error
envelope (this twin uses 'not_found'/'read_only') are twin-chosen, not independently confirmed
per-failure-kind from a fetched source — annotated ⚠ doc-UNVERIFIED, filed as
errors.code_taxonomy.
No mirror
API-first vendor (../../../docs/contributing/architecture.md C1b) — the Svix dashboard is a delivery-inspection/config view,
not an agent navigation target — so this pack ships no mirror, and there are no UI capabilities.
