Obligations register
Every testable promise our legal text makes, what keeps it, and whether it holds today. The terms are the spec: product code must satisfy these rows, and product changes that touch personal data are checked against them. A gap row is not yet kept. If a gap row's wording isn't published yet, the wording waits on a draft branch until the code ships.
Statuses: met: the code keeps the promise, checked against the current code. partial: kept in part; the finding below says what's missing. gap: not kept yet. process: kept by people, not code.
Code references: P = aextra-vibekit/cloudflare/proxy/src, A = aextra-vibekit/apps/admin/src, R = aextra-remail/apps/admin/src, VK = aextra-vibekit repo root, RM = aextra-remail repo root. Line numbers drift, so function names are given where they exist. Test commands under "How to verify" run in aextra-vibekit/cloudflare/proxy unless another folder is named.
Sources: vibekit/terms §9.2 means the VibeKit Terms of Service, clause 9.2. "(draft)" means the wording is on the draft/remail-unsubscribe branch and isn't published yet.
Last verified: 25 September 2026, against aextra-vibekit 8f8914f and aextra-remail b264ac9.
VibeKit
| ID | Obligation | Source | Product | Code / process | Status | How to verify |
|---|---|---|---|---|---|---|
| V-01 | Users accept the current Terms and Privacy Policy before using the admin, and each acceptance is recorded (user, version, time, country, user agent) | vibekit/terms §1; vibekit/privacy §2.1 | VibeKit | A/components/TermsGate.tsx (mounted in A/components/ProtectedRoute.tsx); P/routes/api.ts POST /api/account/terms (409 unless the version is current); P/lib/account.ts recordTermsAcceptance; D1 terms_acceptances (VK/cloudflare/proxy/migrations/0011_terms_acceptances.sql); VK/packages/prompts/src/legal.ts TERMS_VERSION |
met | Sign in with a new account, accept, then SELECT * FROM terms_acceptances in D1 |
| V-02 | Connected credentials are encrypted at rest (AES-256-GCM) and never returned to the browser | vibekit/privacy §2.1; vibekit/privacy §9 | VibeKit | P/lib/crypto.ts (AES-GCM envelope); P/lib/provision.ts; P/lib/auth-config-ops.ts redactCredentials |
partial | GET /api/apps/:id with the owner's token must return no secretEnc or apiKeyEnc fields |
| V-03 | Google sign-in requests only the openid and email scopes, so Google shares only the email address and whether it is verified |
vibekit/privacy §2.3; remail/privacy §2.3 | VibeKit, ReMail | End users: P/routes/auth.ts (scope=openid email). Customers (admin, shared with ReMail): A/lib/adminAuth.ts signInWithGoogle → Supabase Google provider |
gap | Start admin Google sign-in and read scope= in the accounts.google.com URL |
| V-04 | No password sign-in for production accounts | vibekit/privacy §2.1 | VibeKit | A/lib/adminAuth.ts (password path only when import.meta.env.DEV); A/components/SignIn.tsx; Supabase Auth settings |
met | In apps/admin, npm run build then grep -c devSignInWithPassword dist/assets/*.js gives 0. In the Supabase dashboard, the Email provider has password sign-in disabled |
| V-05 | End users' matched records are encrypted at rest, in the database and in the session working copy | vibekit/privacy §2.2; vibekit/privacy §9 | VibeKit | P/lib/session.ts (app_users.record_blob); P/lib/session-snapshot.ts; P/durable-objects/session-channel.ts |
met | npm run test:session-snapshot |
| V-06 | Retention: request and alert logs 30 days; sign-in events 90 days; change log 1 year; device records 90 days after last seen; revoked access 90 days; orphan sign-in identities deleted; unused authorization codes 1 day; revoked authorizations 30 days; form uploads 7 days; edge cache 30 days or less; sign-in links and codes 15 minutes | vibekit/privacy §10 | VibeKit | P/lib/logging.ts (TTLs and sweep); P/lib/retention.ts; hourly cron (VK/cloudflare/proxy/wrangler.toml crons, P/index.ts scheduled); P/routes/data.ts CACHE_MAX_AGE; P/lib/otp.ts; R2 lifecycle on aextra-vibekit-uploads; Supabase magic-link expiry |
partial | npm run test:retention; npx wrangler r2 bucket lifecycle list aextra-vibekit-uploads shows a 7-day rule; Supabase Auth → Email OTP expiry is 900 s or less |
| V-07 | Deleting a project permanently erases its app: sign-ins stop, and configuration, end-user identities and sessions, sign-in history, logs, uploaded files and specifications are deleted | vibekit/privacy §10 | VibeKit | P/lib/app-purge.ts purgeApp (and its late sweep); P/routes/api.ts DELETE /api/apps/:id; A/lib/projects.ts deleteProjectPermanently |
partial | npm run test:app-purge; delete a test project, then look for its appId in D1, KV and R2 |
| V-08 | Self-service account deletion erases every owned app, saved tokens, AI-agent connections and their change log, the product-update list entry, Supabase rows and the sign-in | vibekit/privacy §10 | VibeKit | P/lib/account.ts deleteAccount; P/routes/api.ts DELETE /api/account; A/components/AccountModal.tsx |
met | npm run test:account; delete a test account, then check D1, Supabase and the Resend list |
| V-09 | Emailed deletion requests from the account address are completed within 30 days; customers' requests to delete end-user records are actioned without undue delay | vibekit/privacy §10; vibekit/privacy §11 | VibeKit | Process: someone monitors privacy@aeropage.io and runs the deletion (V-07/V-08 tooling) |
process | Owner confirms who monitors the inbox; keep a log of each request, with the date received and the date done |
| V-10 | Product-update emails: an opt-out tick box at emailed-link sign-up; an on/off switch under My account; opting out or unsubscribing works; opted-out users stay on the list marked unsubscribed and get no onboarding emails | vibekit/privacy §4 | VibeKit | VK/supabase/functions/onboarding-signup (marketing_opt_out); A/components/SignIn.tsx tick box; P/lib/account.ts marketingOptedOut, marketingEmail; P/routes/api.ts /api/account/marketing; A/components/AccountModal.tsx; Resend templates "VK Onboarding · *" |
met | npm run test:account; sign up with the box ticked, then check the Resend contact is unsubscribed and no user.created event fired; open each onboarding template in Resend and check for an unsubscribe link |
| V-11 | End users and form submitters are never added to our lists; form mailing lists use only the customer's own Resend key | vibekit/privacy §2.2; vibekit/privacy §4 | VibeKit | P/routes/data.ts (form → audience only with hasOwnResendMailer); P/lib/email.ts addResendAudienceContact |
met | Submit a form on an app without its own Resend key: the logs show "resend-audience skipped" |
| V-12 | "Describe view" sends the field names and at most 5 records, each value shortened, to Workers AI; customer data is never used to train models | vibekit/privacy §6 | VibeKit | P/routes/api.ts view-describe route (maxRecords: 5, values cut to 80 characters, prompt capped at 4000); models run on Workers AI |
met | Code review of the view-describe route |
| V-13 | The shared design-reference library, its public screenshots, and the fact that we don't record who submitted each reference are disclosed and true | vibekit/terms §12.3; vibekit/privacy §6 | VibeKit | VK/cloudflare/design-enrich/src/index.ts (stores URL and brand only) |
met | Read the stored library row: it has no user id |
| V-14 | The provider list is accurate, and is updated before a new provider processes end-user data | vibekit/privacy §8; vibekit/privacy §12; vibekit/terms §11 | VibeKit | Process + content/shared/subprocessors.md (kept word for word in step by npm run check) |
process | Before adding any binding or API to VK/cloudflare/proxy/wrangler.toml or a new outbound host, check §8 lists the provider |
| V-15 | Customers are notified of a personal-data breach affecting their data without undue delay, and authorities where required | vibekit/terms §11; vibekit/terms §17; vibekit/privacy §9 | VibeKit | Process: needs an incident runbook (who decides, how customers are contacted, the 72-hour regulator clock) | gap | A written runbook exists and names an owner |
| V-16 | The admin loads no Google Fonts, advertising cookies, cross-site trackers or analytics pixels | vibekit/privacy §2.4 | VibeKit | A/main.tsx (@fontsource-variable/inter); VK/apps/admin/index.html (no third-party tags) |
met | Load the admin with the browser's Network tab open: no requests to fonts.googleapis.com, fonts.gstatic.com or analytics hosts |
| V-17 | Quick Proxy links can't be used as an open redirect | vibekit/terms §13.3 | VibeKit | P/lib/quick-redirect.ts decideRedirect (plus interstitial); P/routes/quick.ts |
met | npm run test:quick-redirect |
| V-18 | Customers are warned about risky configuration: legacy-open and Public queries, secret-looking variables, token breadth, the "log in as" secret | vibekit/terms §9.3 | VibeKit | P/lib/endpoint-warnings.ts; P/lib/secret-heuristic.ts; MCP (P/routes/mcp.ts); admin warnings in A/pages/AppDetail.tsx and A/lab/onboarding/Steps.tsx |
met | npm run test:endpoint-warnings |
| V-19 | Sign-ins stop immediately for a deleted or disabled app, everywhere; the rest of the app stops within about a minute | vibekit/privacy §10 | VibeKit | P/routes/auth.ts getActiveApp (on every sign-in route) → P/lib/app.ts getApp |
partial | Disable an app, then try a sign-in within 60 s from a different region |
| V-20 | At least 30 days' notice (best effort) before discontinuing the whole service; reasonable advance notice before charging for a feature in use | vibekit/terms §21; vibekit/terms §15 | VibeKit | Process: owner | process | Any shutdown or pricing plan includes a dated notice email to all customers |
| V-21 | Processor duties: follow instructions only; confidentiality; security as described; help with data-subject requests; delete on request or termination; show compliance | vibekit/terms §11 | VibeKit | Terms §11 + V-07, V-08, V-09, V-14, V-15 | process | Covered by the rows listed |
ReMail
| ID | Obligation | Source | Product | Code / process | Status | How to verify |
|---|---|---|---|---|---|---|
| R-01 | Every live email contains a working per-recipient unsubscribe link, added to the footer if the template has none. It opens a page styled with the sender's brand that offers 3 choices: never email me again (this sender, all projects); remove me from this list (project); don't send me this email again (template) | remail/terms §9.3 (draft); remail/privacy §1 (draft) | ReMail | To build: token in the proxy send path (P/remail/routes/send.ts), public route mounted like /rmedia in P/index.ts, token kind in P/lib/tokens.ts. Templates today use link('Unsubscribe','#') (R/remail/templates/systems/*.ts) |
gap | Send a live test and follow the link: each of the 3 choices is recorded at the right scope |
| R-02 | Every live email carries List-Unsubscribe and List-Unsubscribe-Post headers; one-click unsubscribe means "never email me again from this sender" |
remail/terms §9.3 (draft) | ReMail | To build: the contract P/remail/contracts.ts and checkEmail (P/remail/lib/validate.ts) have no headers; the Resend adapter is P/remail/lib/providers/resend.ts. Check that Resend's batch endpoint accepts per-email headers |
gap | Raw headers of a live test email; POST to the one-click URL records an all-projects suppression |
| R-03 | Suppressed recipients are never sent to. This is enforced by the server at send time (per-email suppressed failure, body unchanged for idempotent retries) and shown in the batch UI |
remail/terms §9.3 (draft); remail/privacy §2.2 (draft) | ReMail | To build: check after hashing in P/remail/routes/send.ts prepareSend; new suppressions migration in RM/supabase/migrations/; suppressed class in R/remail/batching/classify.ts |
gap | Proxy test: send to a suppressed hash, which returns suppressed with no Resend call. Also npm run selfcheck:batching in aextra-remail/apps/admin |
| R-04 | A sender postal address is a required project setting; live sends are blocked until it is set, and it is added to every email footer next to the unsubscribe link | remail/terms §9.3 (draft) | ReMail | To build: R/remail/ProjectSettings.tsx, gate in the proxy send path |
gap | Try a live send on a project with no address: it is refused. The footer of a live test shows the address |
| R-05 | The send log and batch records store only a keyed hash, a masked hint, the subject, status, error and Resend reference, never email bodies | remail/privacy §2.2; remail/privacy §7 | ReMail | P/remail/lib/contact-hash.ts (HMAC of user id + normalised address); RM/supabase/migrations/20260924000000_remail_send_log.sql (no body column); R/remail/sendLog.ts; remail_batches.planned holds {h, hint} only |
met | npm run test:remail-contact-hash; \d remail_send_log in the database |
| R-06 | The Airtable token and Resend key are encrypted at rest (AES-256-GCM) and never returned to the browser | remail/privacy §2.1 | ReMail | P/remail/routes/keys.ts (responses are only {keyId} or {ok}); P/lib/provision.ts; P/lib/crypto.ts; user_keys stores only secret_id |
met | npm run test:remail |
| R-07 | In the customer's Airtable, ReMail only creates one batch-log record per batch and only deletes simulation (rmsim:) batch-log records. It never changes or deletes the customer's own records, and the server enforces this |
remail/terms §12.1 | ReMail | Client: R/remail/batching/marker.ts, R/remail/batching/clearSimulations.ts, R/remail/batching/liveDeps.ts. Server: P/remail/routes/airtable.ts /records/delete and /records/create accept any ids or records |
partial | Proxy test (npm run test:remail-airtable): /remail/airtable/records/delete with a non-rmsim: record id is refused |
| R-08 | Users can delete their saved keys, logos, brands and themes, projects (permanently) and their account themselves | remail/privacy §8 (draft) | ReMail | To build: key delete (proxy DELETE /remail/keys/:keyId exists; R/lib/projects.ts deleteUserKey is unused and misses Resend keys); logo delete (R/lib/brandAssets.ts only uploads); R/lib/themes.ts deleteTheme is unused; project delete is trash only (trashProject); account delete is a mailto (R/components/AccountModal.tsx); shared P/lib/account.ts deleteAccount misses Logos/<uid>/* |
gap | Delete each item in the UI, then confirm it's gone from Supabase tables and storage |
| R-09 | Suppressions, the send log and batch records are kept for the life of the sender's account and deleted with it | remail/privacy §8 | ReMail | Send log and batches: on delete cascade from auth.users (RM/supabase/migrations/20260924000000_remail_send_log.sql). Suppressions: table not built yet |
partial | Delete a test auth user: its remail_send_log, remail_batches and (later) suppression rows are gone |
| R-10 | Users accept ReMail's Terms and Privacy Policy before use, by ticking a box and clicking accept, and the acceptance is recorded (user, version, time, country, user agent) | remail/terms §1 (draft) | ReMail | Today: notice only on sign-in (R/components/SignIn.tsx). To build: reuse POST /api/account/terms with version remail:<date> and app_metadata.remail_terms_version |
partial | Sign in with a new account: the pop-up can't be dismissed, and a terms_acceptances row with remail: appears |
| R-11 | Product-update emails: an opt-out tick box at ReMail sign-up and an on/off switch under My account | remail/privacy §4 (draft) | ReMail | To build: R/components/SignIn.tsx, R/lib/attribution.ts signupMetadata (add marketing_opt_out), shared /api/account/marketing |
gap | Sign up with the box ticked: the Resend contact is unsubscribed and gets no onboarding |
| R-12 | The ReMail admin's own interface loads no Google Fonts; brand fonts customers choose for emails are disclosed | remail/privacy §2.4 (draft) | ReMail | R/index.css line 1 imports Inter from fonts.googleapis.com; replace with @fontsource-variable/inter. Customer brand fonts: R/remail/brand/fonts.ts |
gap | Load the admin with the Network tab open: no fonts.googleapis.com or fonts.gstatic.com requests outside brand-font previews |
| R-13 | "Contact support" either really sends the message, or isn't offered | remail/privacy §2.1 | ReMail | R/components/ContactDialog.tsx pretends to send (logs, waits, shows "sent"); reached from HelpMenu |
gap | Submit the form: a message arrives at the support inbox |
| R-14 | Starter templates contain no real people's photos or third-party brands | remail/terms §13.2 | ReMail | R/remail/editor/components/ecommerce.tsx (GitHub avatars), articles.tsx and socialProof.tsx (Jobs and Wozniak photos), brand and app-store assets in RM/apps/admin/public/remail/static/ |
gap | Search R/remail and RM/apps/admin/public/remail for github.com avatar URLs, steve-, Braun, Herman Miller, App Store and Google Play: nothing is found |
| R-15 | No VibeKit or Aeropage leftovers in the ReMail UI; correct LICENSE | owner decision (LEGAL-REPO-PLAN §0) | ReMail | R/lib/adminAuth.ts, R/components/AccountModal.tsx, R/lib/devMode.ts (team@aeropage.io), RM/LICENSE.md ("Copyright 2020 Airtable"), /admin/design routes in R/App.tsx, RM/apps/admin/public/embed.js |
gap | Search R for "VibeKit" and "aeropage": nothing is found; LICENSE names the right owner |
| R-16 | No unused ReMail legacy routes on the proxy | owner decision (LEGAL-REPO-PLAN §3) | ReMail | P/routes/api.ts "ReMail LEGACY" routes (/keys/resend, /resend/send, /airtable/view, /airtable/records-by-ids, /keys/:keyId/verify-airtable); EXPECTED_REACH_INS in VK/cloudflare/proxy/scripts/remail-boundary.selfcheck.mjs |
gap | npm run test:remail-boundary; searching P/routes/api.ts for LEGACY finds nothing |
| R-17 | Before a live send, a checklist shows the recipient count, confirms unsubscribe and postal address are included, and says the send can't be undone | remail/terms §9.5 | ReMail | R/remail/batching/ui/BatchProgressDialog.tsx (confirm step and "SEND ALL" gate) has counts and a typed confirmation, but no unsubscribe or address checks and no "can't be undone" wording |
partial | Open the live-send confirm step and check each item is shown |
| R-18 | Emailed deletion requests (projects, credentials, logos, send history, account) from the account address are completed within 30 days, including logos in storage | remail/privacy §8 | ReMail | Process: someone monitors privacy@aeropage.io. The shared deleteAccount doesn't remove Logos/<uid>/*, so logos are deleted by hand |
process | Keep a log of each request, with the date received and the date done; confirm the Logos/<uid>/ folder is empty afterwards |
| R-19 | Images from Airtable records are cached for at most 1 day | remail/privacy §2.2; remail/privacy §8 | ReMail | P/remail/routes/media.ts (cache-control: public, max-age=86400) |
met | curl -sI an /rmedia/... URL and check max-age=86400 |
| R-20 | ReMail adds no open or click tracking to emails | remail/privacy §2.2 | ReMail | P/remail/lib/providers/resend.ts resendPayload (no tracking options; tracking is a setting on the customer's Resend domain) |
met | Code review of resendPayload |
| R-21 | Server logs contain no email addresses, email content or record data | remail/privacy §7 | ReMail | P/remail/index.ts logs method, route, error name and one stack frame only. The global onError in P/index.ts logs whole error objects, which a legacy route error could fill (fixed by R-16) |
partial | Every console. call under P/remail logs no request data; after R-16, no ReMail path reaches the global onError |
| R-22 | Recipients are never added to our own mailing lists or used for our own purposes | remail/privacy §3 | ReMail | The ReMail plane can't import the platform list helpers: P/lib/email.ts is not on the import allowlist in VK/cloudflare/proxy/scripts/remail-boundary.selfcheck.mjs |
met | npm run test:remail-boundary |
Shared
| ID | Obligation | Source | Product | Code / process | Status | How to verify |
|---|---|---|---|---|---|---|
| S-01 | legal@aeropage.io and privacy@aeropage.io are monitored, including after the Aeropage sunset (1 October 2026) |
vibekit/privacy §13; remail/privacy §11; vibekit/terms §27; remail/terms §27 | All | Process: owner | process | Owner sends a test message to each address and confirms it arrives |
| S-02 | The contracting entity's registered name is correct ("Aeropage Limited" or "… LLC") | vibekit/terms §27; remail/terms §27 | All | Counsel review (see aextra-vibekit docs/LEGAL-REVIEW-2026-09.md) |
process | Counsel confirms against the Delaware registration |
| S-03 | The Google OAuth consent screen's privacy-policy link points to the live policy | vibekit/privacy §2.3 | All | Owner: Google Cloud Console → APIs & Services → OAuth consent screen | process | Open the consent screen during Google sign-in and follow its privacy link |
Findings from the 25 September 2026 verification
These are the reasons behind every partial or gap row that the seed list had as met, plus problems found along the way. Where the published text says something the code doesn't do, the fix is in the code (build to comply), unless the owner decides the text should change instead.
- V-03 (published text is not true today): customers who sign in to the admin (VibeKit or ReMail) with Google go through Supabase's Google provider. It always asks Google for
email profile, so we receive the person's name and avatar URL too.VK/supabase/functions/onboarding-signupthen readsfull_nameand sends it to Resend with the contact. Only end-user sign-in (P/routes/auth.ts) asks foropenid email. To comply: stop passing the name to Resend, and move admin Google sign-in to a flow that requests onlyopenid email. Alternatively, the owner changes privacy §2.3. - V-06 (published text is not true today): customer sign-in links for the admin expire after 1 hour. The email text in
P/routes/auth-email-hook.tssays so, and that is Supabase's default. Privacy §10 says 15 minutes. To comply: set Supabase Auth's email OTP expiry to 900 s and change the email text. Also, the 7-day lifecycle on theaextra-vibekit-uploadsR2 bucket exists only as a comment inwrangler.toml. Check it withnpx wrangler r2 bucket lifecycle list aextra-vibekit-uploads. - V-02:
GET /api/apps/:id(P/routes/api.ts) returns the whole appconfig, including the encryptedauth.google.secretEncandauth.mailer.apiKeyEnc. It is ciphertext, not the secret, but it skipsredactCredentials. The "256" in AES-256 depends on the length of theENCRYPTION_KEYsecret, which the code does not check. - V-07: projects trashed before 25 September 2026 still have live apps; no one-off purge script exists yet.
project_teamrows rely on a foreign-key cascade that isn't visible in the repo. - V-10: the sign-up opt-out works only for emailed-link sign-up, which is exactly what privacy §4 says. Google sign-ups can use the My account switch. The unsubscribe links in the Resend onboarding templates can only be checked in Resend.
- V-19:
getActiveAppreads through a 5-second in-memory cache and KV. In another region a disabled app can still accept sign-ins for up to about 60 seconds. Privacy §10 says sign-ins stop "immediately". - V-04: the admin build has no password sign-in. Whether the Supabase project itself still accepts passwords is a dashboard setting; the owner should check it.
- V-20: the Terms say we will "try to give" 30 days' notice. The obligation is recorded as that best-effort promise.
- R-07: the published §12.1 promise holds for the ReMail app as shipped, but the proxy would delete or create any record that a caller asks for with their own token. Server-side enforcement is Phase 3.
- R-14: besides the licensing issue, recipients' mail apps load the starter-template avatars from github.com, which is not in ReMail's provider list.
- ReMail privacy §2.4 ("Our pages load fonts from Google Fonts") is true today. It changes with R-12, and the draft wording covers it.
- Stale reference fixed: the seed list pointed V-14 at
shared/subprocessors.mdin the product repo. It lives here, ascontent/shared/subprocessors.md.