All notable changes to this project will be documented in this file.
Note: This file is the source of truth.
apps/docs/src/content/changelog.mdis auto-generated from this file on every docs build viaapps/docs/scripts/build-content.jsand is gitignored. Do not edit the generated file directly — changes will be wiped on the next build.
overview/webhooks.md now lists proposal.resolved with a dedicated payload section. (The webhook event list also lives in info.description of the OpenAPI spec — a known drift, logged for a future hygiene sweep.)sdks/proposals-widget.md documents poll-mode tokens (named vs open), the voter self-identification form for open tokens, and which widget callbacks fire vs don't in poll mode..ics content shape (title hard-coded Vennio meeting, description format, ATTENDEE / PARTSTAT polish) and status: 'accepted' reuse for resolved polls — code defensively, refinements will ship with a deprecation window.401 invalid_token across all token-auth endpoints. GET /v1/proposals/by-token and GET /v1/proposals/{id}/ics previously returned 404 invalid_token on unknown tokens; they now return 401 {error:'unauthorized', detail:'invalid_token'}, matching POST /v1/proposals/{id}/response which already 401'd. One coherent rule: magic-link tokens are auth credentials, not resource lookups, so credential failures uniformly return 401. Deliberate behaviour change, made while there are no live consumers. (PR #TBD)api/openapi.yaml brought up to date for Stages 1-6a. Pure documentation of live code behaviour — no behaviour change beyond the 401 normalisation above. Proposal gains proposer_name, proposer_email, team_id; ProposalParticipant gains full_name, email, and user_id becomes nullable (open voters). Proposal.status description now includes the poll-mode lifecycle (reaper two-pass, resolution reuses accepted, open polls resolve only via close or expiry). CreateProposalRequest documents the poll-mode expiry floor (earliest_slot_too_soon 400), expires_in_hours being ignored in poll mode, and the metadata.team_id promotion. Per-endpoint error codes added: 400 earliest_slot_too_soon on create; 400 poll_mode_only_supports_accept, 401 missing_credentials, 401 participant_unresolved, 403 not_a_participant, 409 you_have_already_responded on response; full error matrix added on by-token. Prerequisite to Stage 8 (docs + llms.txt generate from this spec).proposal.resolved webhook event. Fires when a poll-mode proposal resolves to a winning slot — covers both named polls (all-voted trigger) and open polls (organiser close, and the future expiry reaper). Payload: {proposal_id, thread_id, poll_variant, winning_slot:{id, start, end}, vote_count, total_votes, tie_count}. Delivered to the organiser's webhook subscribers (Model 1: the API knows the votes, the builder fans out to voters by their preferred channel). (PR #TBD)GET /v1/proposals/{id}/ics?token=... — returns the resolved poll as a text/calendar (.ics) file so a voter can tap a magic-link URL and add the meeting to their calendar without needing email. Token auth mirrors GET /v1/proposals/by-token (the magic-link token IS the credential). 409 poll_not_resolved until the poll resolves; 410 token_expired after the token's expiry. Deterministic iCal UID proposal-{proposalId}-slot-{slotId}@vennio.app — every retrieval of the same resolved poll produces the same UID, so calendar clients can reconcile organiser-side updates against a single shared event identity.ATTENDEE roster on the .ics lists voters who voted (selected_slot_id IS NOT NULL) with a resolvable email; the organiser is the ORGANIZER. Anonymous voters and non-voting invitees are omitted (RFC 5545 expects a CALAddress; mirrors the Model 1 boundary). Title, description, and RSVP/PARTSTAT polish are provisional — validated against real Apple/Google/Outlook clients in Job 2; expect refinements before GA.mode: "poll" and no participant_emails and the 201 returns a single magic_link_url instead of per-participant tuples. Anyone with the link can vote; voters self-identify at vote time via optional voter_name / voter_email on POST /v1/proposals/{id}/response. Email voters can revise their vote (re-POST same email updates the existing row); anonymous voters can't revise and can double-vote — the API gives basic resistance, stricter identity is the builder's surface to harden. Distinguished from named polls (per-participant magic links, Stages 2-4) via the new poll_variant: 'named' | 'open' field on Proposal. (PR #444)POST /v1/proposals/{id}/close — organiser-explicit close of a poll. Required for open polls (which have no all-voted trigger since the voter set is open-ended) and useful for named polls when the organiser doesn't want to wait. With ≥1 vote → resolves to the winning slot (ties broken by earliest start_time); zero votes → expires. Poll-mode only; standard proposals use /cancel (different semantics — cancel deletes calendar events from an accepted proposal, close terminates a still-proposed poll).GET /v1/proposals/by-token now returns participant: null for open-poll tokens (token.kind: 'open'); client UI must render a name/email field and create the participant on submit. For named tokens (token.kind: 'named') participant is always populated as before.POST /v1/proposals/{id}/response — 20 votes per 60 seconds per token by default, env-tunable via OPEN_VOTE_RATE_LIMIT_MAX / OPEN_VOTE_RATE_LIMIT_WINDOW_MS. Returns RFC 7807 on 429.proposals.poll_variant column added ('named' | 'open', NOT NULL when mode='poll'). Set at create time; every Stage 5+ branch reads this field rather than inferring mode from token shape.proposal_action_tokens — participant_id is now nullable, new kind column ('named' | 'open'), the original UNIQUE (proposal_id, participant_id) replaced with two partial UNIQUE indexes (one per kind). Open tokens are multi-use (skip the used_at flip on consume).proposal_participants identity CHECK relaxed to accept display_name IS NOT NULL so fully-anonymous voters pass (server synthesises display_name='Anonymous' when neither voter field is supplied).502 calendar_unavailable — misleading, because the user's calendar was perfectly fine and Google was never called. The pin captured at create-time was simply still null. Accept now re-resolves the participant's primary calendar on the fly, persists it onto the participant row, and proceeds. (PR #400)400 participant_calendar_not_pinned (with a human-readable message) rather than a generic 502. calendar_unavailable is now reserved for genuine upstream Google failures, making future debugging less misleading.app.vennio.app) cutover from Vercel to Azure Static Web Apps. The React app is now served from the vennio-portal SWA in West Europe. The Vercel project is retained as a rollback target only — no new Vercel-specific config should be added. (PRs #386, #384, #385, #399)vennio.app) and docs (docs.vennio.app) on SWA finalised. staticwebapp.config.json is now served from each app's public/ directory so SWA picks it up at build time. Marketing-site wildcard redirects had their trailing /* dropped from destinations (Azure SWA rejects them). (PRs #395, #398, #382)vennio.app/* and have moved to app.vennio.app/* now 301 at the marketing edge so external links and email templates don't break. (PR #394)api.vennio.app CORS allowlist now includes app.vennio.app so the cutover portal can call the API. (PR #389)app.vennio.app ahead of cutover. (PR #388)vennio-api.service systemd unit fixed. WorkingDirectory=/opt/vennio/api pointed at an empty directory after the source-tree move; CI deploys were bypassing the unit by calling docker compose directly, masking the drift. The unit is now under deploy control with the correct path — systemctl restart and reboots will actually work. (PR #383)/src to /apps/portal/ for monorepo symmetry with apps/marketing and apps/docs. (PR #384)PORTAL_URL, plus a centralised getPortalUrl() helper. Previous names (VITE_PORTAL_URL, FRONTEND_URL, APP_URL, etc.) drifted across services and were a source of inconsistent OAuth redirect URIs and email link bases. Operators running self-hosted deployments should set PORTAL_URL and remove the legacy variables. (PR #387)infra/README.md trimmed from 284 lines to ~50. The duplicated operational sections (env vars, viewing logs, manual cron testing, rollback, troubleshooting) are removed — those live in docs/deployment/DEPLOYMENT.md. What remains: a tight IaC file inventory + the architecture diagram + the source→VM path mapping for each file. Each file's status is now explicit (active vs. historical vs. stale).infra/staging.md banner clarifying the staging environment is not implemented (no vennio-api-staging-rg exists), and flagging the doc's drifted facts (B2s vs actual D2s_v6, /opt/vennio/api/ vs actual /source/, UK South vs actual West Europe). If staging is stood up in future it should be redone from current state, not from these steps.infra/cutover-env-vars.md banner clarifying the cutover this doc was written for completed months ago. Pointed at the audit doc's "Env vars actually set on the VM" section as current truth; flagged the drift items (most Paperclip keys, WEBAPP_URL, API_BASE_URL, APPLICATIONINSIGHTS_CONNECTION_STRING etc. listed in template but not on VM).docs/projects/marketing-site.md — DNS provider question resolved (Porkbun, confirmed in the audit). Pointer added to the audit doc as the verified infrastructure source.docs/adr/002-domain-split.md — pointer added to the audit doc in the Related references.No code changes. No content changes other than banners + the README rewrite. This PR just stops competing sources of truth.
docs/deployment/DEPLOYMENT.md from evidencedocs/deployment/DEPLOYMENT.md rebuilt from scratch. The previous version had drifted (path errors, missing surfaces, missing Function App for docs). The new doc was assembled via a methodical audit: every claim is dated 2026-05-17 and traceable to a verification command, listed in a verification log at the end./opt/vennio file layout, all 6 cron systemd timers, marketing + docs SWAs, docs Function App, full DNS record map (Porkbun nameservers), every third-party service in use (Supabase, Google/MS/Salesforce/HubSpot OAuth, Stripe, Resend, Anthropic, Firecrawl, Sentry, PostHog, Paperclip, Tailscale, GHCR, Application Insights), GitHub Actions workflows + required secrets.vennio-api.service systemd unit (WorkingDirectory=/opt/vennio/api points at an empty directory; CI deploys bypass it via direct docker compose, but systemctl restart or VM reboot would fail), the EXTRA_ALLOWED_ORIGINS duplicate entry, stray source-tree artefacts on the VM including unexpected .env.local/.env.vercel.local files that should not be on production hardware, missing APPLICATIONINSIGHTS_CONNECTION_STRING, and the Stripe webhook destination drift noted in CLAUDE.md.infra/README.md gets a banner clarifying it documents the original Vercel → Azure migration sequence (historical) and pointing at the new deployment doc for current operations.No code changes. No infrastructure changes. The audit identified work items; this PR ships the durable record.
apps/marketing/src/pages/scheduling-audit.astro) ported from src/pages/SchedulingAudit.jsx. Five-question form, A–F scoring across multi-party / conflict / timezone / cancellation / observability dimensions, plain-text report, email-capture lead form posting to POST /v1/audit-leads. Logic verbatim; visual surface refit to the editorial design language (Playfair headlines, mono Q1–Q5 eyebrows, hairline-bordered radio options with amber-accent selected state, ink terminal block for the report).apps/marketing/. The interactive form is a React component rendered as a client:load island inside the Astro page. Added @astrojs/react + react + react-dom to the marketing site. The static shell (hero, headings, design language) remains zero-JS; only the audit form hydrates.The audit form POSTs to https://api.vennio.app/v1/audit-leads. The API's CORS allowlist accepts vennio.app, www.vennio.app, docs.vennio.app, localhost. On SWA PR preview URLs (<host>-<PR#>.westus2.7.azurestaticapps.net) the call will fail until EXTRA_ALLOWED_ORIGINS on the API VM is updated. Production is fine.
Phase 2 progress: 5 of 5 marketing pages ported. Developers, Waitlist, vibe-coders, GetStarted dropped (dead / auth flow). Phase 2 complete after this PR merges.
/ is now the homepageapps/marketing/src/pages/index.astro) ported from src/pages/LandingPage.jsx. Six sections: hero ("Infrastructure for Time" with italic-amber accent on "Time"), About Vennio (compliance copy), What it is (Stripe analogy), How it works (three step cards), Why Vennio (five differentiator rows), CTA band. Content verbatim; presentation uses the established editorial design language (Playfair regular headlines, mono eyebrows, hairline section dividers, ink/paper/amber palette)./ → /blog 302 redirect removed from staticwebapp.config.json. The marketing site now serves an actual homepage at vennio.app/. Phase 1 placeholder behaviour retired.SITE.external.auth.Known copy tension flagged for follow-up (not changed here): the homepage "What it is" section retains "Scheduling infrastructure for developers" verbatim from the SPA, while the new /about copy uses "Scheduling infrastructure for builders." Deliberate verbatim port — the homepage messaging is a separate editorial decision, not a port concern.
Phase 2 progress: 4 of 6 marketing pages ported (Privacy, Terms, Pricing, Landing). Still pending: Developers, SchedulingAudit.
apps/marketing/src/pages/pricing.astro) ported from src/pages/Pricing.jsx. Four flat-rate tiers — Free, Indie £29, Builder £99, Scale £299 — with "Most Popular" amber accent on Indie. Eleven-row feature comparison table. Content unchanged. Visual presentation aligned to the marketing editorial design language; ● (amber) and — replace the ✅/❌ emoji for a less casual look.SITE.external.auth constant added to lib/site.ts. Every "Get Started" / "Sign in" CTA on the marketing site now reads from this single source — flips one line at Phase 4 cutover from vennio.app/auth to app.vennio.app/auth. Header Sign in button and all three Pricing CTAs use it.Phase 2 progress: 3 of 6 marketing pages ported (Privacy, Terms, Pricing). Still pending: Landing, Developers, SchedulingAudit.
apps/marketing/src/pages/privacy.astro) and Terms of Service (apps/marketing/src/pages/terms.astro) ported from the SPA (src/pages/Privacy.jsx, src/pages/Terms.jsx). Content verbatim; visual presentation aligned to the marketing site's editorial design language (Playfair Display headlines regular weight, DM Sans light body, mono-uppercase eyebrows, hairline section dividers, no shadows). Service-provider table on Privacy updated: Vercel → Azure (West Europe) to reflect the API + docs migration that's already shipped.LegalLayout.astro with the back-to-home link, eyebrow + title + last-updated header, and prose-styled body. Reusable for any future legal page (DPA, sub-processor list, AUP).Phase 2 progress: 2 of 6 marketing pages ported. Still pending: Landing, Pricing, Developers, SchedulingAudit. Waitlist + vibe-coders + GetStarted dropped from the port list (dead / auth flow).
apps/marketing/. Astro 5 SSG project, deploys to Azure SWA at vennio-marketing (West Europe, Standard tier). Independent of the workspace; mirrors the apps/docs/ deploy pattern. Same brand tokens, fonts (Playfair Display / DM Sans / DM Mono), and editorial design language as the existing vennio.app React SPA — visual continuity for the cutover (ADR-002)./blog/best-scheduling-api-for-developers-2026. 6000-word comparison pillar covering 13 scheduling/calendar APIs. Carries Article + ItemList + FAQPage + BreadcrumbList + Organization + WebSite + Person JSON-LD; passes Google Rich Results Test on all five surfaces./ (302 → /blog), /blog index with Pagefind client-side search, /blog/[...slug], /about, /rss.xml, /sitemap-index.xml, /robots.txt, /llms.txt.src/lib/og.ts renders 1200×630 PNGs via satori + resvg at build time; per-post OGs auto-generated from frontmatter so future posts ship with the right social card without manual asset production. Title font scales 76→52→44px based on character count to avoid overflow.Vennio Design System/brand/monograms.html. Four assets (favicon.svg, apple-touch-icon.png, icon-192.png, icon-512.png) all generated programmatically from the same satori pipeline.vennio.app becomes the marketing site (Astro SSG) at Phase 4 cutover; portal SPA moves to app.vennio.app. Decision documents: target structure, redirect map for all portal routes, OAuth whitelist checklist (ADR-001 dependency), phased migration plan, Phase 4 rollback strategy (90-day Vercel warm-keep with explicit rollback triggers).docs/projects/marketing-site.md. Single source of truth: phase tracker, decisions log, open items, verification log. Resumable cold by any agent.vennio.app is still the Vercel SPA. SWA preview URL: https://delightful-grass-0c36f151e.7.azurestaticapps.net (production hostname; PR previews carry a -<PR#> suffix and X-Robots-Tag: none).src/pages/ into apps/marketing/. Phase 3–5 cover OAuth whitelist updates, DNS cutover, and the 90-day deprecation window.@vennio/proposals-widget pagedocs.vennio.app/sdks/proposals-widget (source). Documents the demand-side embed widget — quick start, options, programmatic usage, event payload shapes, React companion, security/threat model for the token in the DOM, and a "when to use this vs. the raw REST endpoint" framing. Sibling to the JavaScript / Python / React SDK pages under "SDKs & TOOLS".build-content.js scans src/content/**/*.md and regenerates content-manifest.json). Pre-rendered into the static build.@vennio/widget@0.1.4 + @vennio/react@0.3.1 — repository URL refresh@vennio/widget@0.1.4 and @vennio/react@0.3.1 — patch bumps to correct repository.url. Both packages pointed at the long-defunct https://github.com/vennio-app/vennio (the old org/repo name). Now: git+https://github.com/vennio-hq/my-porter.git in the npm-normalised form. No code change — metadata-only.publish warning on @vennio/react@0.3.0 (the version published earlier today) and @vennio/widget@0.1.3 (last published 4 months ago). Fixing in tandem so both shipped packages stop redirecting npm users to a 404.@vennio/proposals-widget@0.1.1 — its repository.url was correct at first publish.POST /v1/proposals/{id}/response description now opens with a "For browser embeds: use the widget, not this endpoint" callout. Surfaces @vennio/proposals-widget and <VennioProposal> from @vennio/react as the recommended path for browser-side recipient responses, with the raw REST endpoint reserved for server-side flows (webhooks, schedulers, adapters). Closes the discoverability gap where agents reading the spec went straight to writing fetch code and rebuilt the the-cut/safe-space mistake (calling the response endpoint with the organizer's API key → 403 organizer_cannot_respond).docs.vennio.app/llms-full.txt and the rendered docs site via the existing build pipeline on next deploy.@vennio/react@0.3.0 — <VennioProposal> component@vennio/react@0.3.0 — minor bump for the new <VennioProposal> component exported on 2026-05-15. The component embeds vennio.app/p/:token in an iframe and surfaces accept/counter/reject postMessage events as React callbacks. Same accept/counter/reject payload shapes as @vennio/proposals-widget. Inlined iframe with useEffect cleanup — no listener leak (the bug fixed in @vennio/proposals-widget@0.1.1 never affected the React component).BookingCalendar / BookingForm / TimeSlotPicker / BookingConfirmation components.@vennio/proposals-widget@0.1.1 — listener-leak fixinit() no longer leaks message listeners on re-mount. Each call to init(container, options) registered a fresh window.message listener but never removed earlier ones bound to the same container. SPAs that re-initialise on token/prop change saw callbacks fire N times for one event. Surfaced by the 2026-05-15 manual iframe test (Mount clicked twice → onAccepted fired twice). Fix: track active handlers per container in a WeakMap and remove the prior listener before binding a new one.init() now returns an unsubscribe function. Backwards-compatible (callers ignoring the return are unaffected). Lets host apps manually tear down the widget; documented in packages/proposals-widget/README.md. The React <VennioProposal> component was unaffected by the leak (its useEffect cleanup already removed the listener) — no change there.@vennio/proposals-widget (demand-side embed widget)Backfilled entry. PR #366 merged without a CHANGELOG entry because the R4 changelog-gate's tier-1 path list does not yet cover
packages/proposals-widget/,packages/react/,packages/widget/, or the main app atsrc/. Gap surfaced in audit follow-up; gate-widening tracked separately.
@vennio/proposals-widget@0.1.0 (new package). Drop-in iframe widget that embeds Vennio's existing magic-link recipient view (vennio.app/p/:token) into any third-party site with 2 lines of HTML, mirroring the supply-side @vennio/widget pattern. Removes the 403 organizer_cannot_respond footgun: embedding apps pass a token and react to postMessage events; they never call POST /v1/proposals/{id}/response themselves, so they cannot accidentally call it as the wrong principal. Three callbacks (onAccepted / onCountered / onRejected) plus a generic onEvent for terminal/error states (expired, used, not_found, calendar_disconnected, error). IIFE 2.57 KB, ESM 2.10 KB, CJS 2.57 KB. Origin-verified postMessage. Auto-init + MutationObserver. Not yet published to npm — see Phase A4 follow-up.@vennio/react — new <VennioProposal> component. React wrapper for the same flow. Inlined iframe (no inter-package runtime dependency). Same callback surface as the vanilla widget. Will publish as @vennio/react@0.3.0 when ready.vennio.app/p/:token)action: counter with counter_slots. New COUNTERING / COUNTERED states in the page state machine. Previously the page linked recipients into the portal to counter; the embedded widget now supports counter end-to-end without leaving the iframe.window !== window.parent), the page now posts vennio:proposal:* events to the parent on accept / counter / reject / expired / used / not_found / calendar_disconnected / error. This is the load-bearing prerequisite for the widget's callback surface. Target origin is '*' because third-party embeds cannot enumerate parent origins; the widget itself verifies event.origin on the receive side.audit/0c-doc-prose-vs-agent-behaviour-2026-05-15.md (this morning's falsification of the doc-prose intervention). Spec at audit/0c-proposals-widget-design.md (PR #365).POST /v1/proposals/{id}/response endpoint now explicitly state that the principal who created a proposal cannot accept, counter, or reject it. Previously the rule was implicit in the dual-auth model (Bearer auth for organizer-side reads; magic-link token for recipient responses) and discoverable only by hitting a 403 mid-build. The 403 error code (organizer_cannot_respond) is now in the documented error code list alongside the existing 401/409 entries.No code or behaviour change — documentation only. Spec change propagates to docs.vennio.app/llms-full.txt and the rendered docs site via the existing build pipeline on next deploy.
@vennio/sdk@0.3.0 published to npm. Second curated-surface release. The
package is the recommended way to talk to the Vennio API from JS/TS apps and
AI agents.vennio.availability namespace with two methods:slots(params) — bookable time slots for a single business or principal
in a date range. Returns a discriminated union (kind: 'flat' for a flat
slots[] array, kind: 'grouped' for slots_by_day keyed by ISO date
when group_by_day: true is passed). Supports public, authenticated, and
consent-scoped access modes; the consent audit-trail field on the
response populates only in consent-scoped mode.findOverlap(params) — mutual availability across 2-10 principals.
Returns the full intersection plus a consents array proving the caller
had authorization from each participant. Empty overlap returns 200 with
slots: [] and a populated consents array (verified via production
probe 2026-05-11).network, eventTypes, consents, apiKeys, invitations,
accessRequests, hubspot, schedules) are now documented in the README
with usage examples for vennio.raw.<namespace>.<method>. Closes the
failure mode where Cursor/Copilot would recommend vennio.eventTypes.create(...)
— which doesn't exist on the modern class — instead of the working
vennio.raw.eventTypes.create(...).vennio.raw.offers (3 methods: create, list, respond). The
underlying /v1/offers/* routes were retired in May 2026; calls would have
returned 404. The SDK's generated client now omits them.vennio.raw.salesforce (7 methods). All /v1/salesforce/* routes are
deprecated: true in the spec; the Salesforce integration is being sunset.
If you currently depend on the Vennio Salesforce integration, contact the
team — the surface is being replaced.sdk-release.yml) now runs a tag-sync guard
before publish. It fails the release if the pushed sdk-v* tag and
packages/sdk/package.json:version disagree. Closes the
0.2.0-vs-0.2.0-rc.1 mislabel class of bug structurally rather than relying
on manual discipline at release time.parser.filters.deprecated: false in
packages/sdk/openapi-ts.config.ts) so any future routes marked
deprecated: true in api/openapi.yaml automatically drop out of the
SDK's public type surface on the next build.user_id query parameter. Dashboard integrations are unaffected. Programmatic integrations need to call POST /v1/oauth/connect-token before initiating OAuth at /v1/calendars/google/connect or /v1/calendars/microsoft/connect.POST /v1/venn-links/{id}/book) now references a new BookingConfirmation schema (5 fields) instead of the canonical Booking schema. The endpoint's behaviour hasn't changed — the spec now accurately reflects what it returns.GET /v1/bookings/stats and GET /v1/api-keys/{keyId}/stats schemas updated to match handler output. The unused last_7_days field has been removed from API key stats responses.redirectUrl field returned by POST /v1/calendars/google/exchange has been renamed to redirect_url for consistency with the rest of the API surface.duration_minutes across all availability and venn-link endpoints — now accepts any positive integer up to 480GET /v1/stripe/session-status, GET /v1/billing/invoices, GET /v1/calendars/connections, GET /v1/calendars/google/connect-guestGET /v1/stripe/session-status made public per original intent — was incorrectly auth-gated, now rate-limited at 5 req/min/IPPOST /v1/access-requests made public per original intent — was incorrectly auth-gated, now rate-limited at 5 req/min/IPGET /v1/invitations/{id} caused by missing validateUUID importPOST /v1/invitations/bulk spec to match handler — request body field names corrected after stale rewrite in d3db26bprincipal_id migration complete: now accepted as alias for business_id across all booking, availability, venn-link, offer, invitation, and booking-series endpointsbusiness_id query param from GET /v1/bookings/stats spec — handler always uses req.userIdGOOGLE_CLIENT_SECRET, MICROSOFT_CLIENT_SECRET) no longer use the VITE_ prefix — prevents accidental exposure in the browser bundlecheck-env.html) no longer displays VITE_GOOGLE_CLIENT_SECRETapplication_fee_amountenrich_meeting calendar enrichment, and Smithery tool annotations/audit page with wizard flow, audit leads API, and personalised fix guide emails/api/og/report-card endpoint for dynamic social sharing imageserror.message in production — details routed through Sentry onlyconsole.log to structured Winston logger with Sentry error transportcalendar.events scope, added regression testsinitialize and tools/list for registry discovery.env.example added with all 30+ environment variables documentednpx vennio CLI with init wizard, status, links, bookings, and test commandsvennio-tokens.css)npm run sync-tokens to propagate token changes to docs site.vercelignore to prevent hobby plan serverless function limit (12 max)