Skip to content

feat(vercel): Vercel provider — auth, deploy engine, Function (async + Effect), and the platform surface - #1217

Draft
sam-goodwin wants to merge 13 commits into
mainfrom
feat/vercel
Draft

feat(vercel): Vercel provider — auth, deploy engine, Function (async + Effect), and the platform surface#1217
sam-goodwin wants to merge 13 commits into
mainfrom
feat/vercel

Conversation

@sam-goodwin

Copy link
Copy Markdown
Contributor

Infrastructure-as-Effects support for Vercel, at parity with the Cloudflare provider where the platform allows. Built wave-by-wave against a real Pro team — 60+ live tests, each service's API mismatches fed back as typed-error patches to distilled (companion: alchemy-run/distilled#457, submodule bumped here).

// Effect-native Fluid function: HTTP + queue consumer + cron in one class
export default class Api extends Vercel.Function<Api>()(
  "Api",
  { main: import.meta.url, resources: { memory: "performance", maxDuration: 60 } },
  Effect.gen(function* () {
    const flags   = yield* Vercel.ReadEdgeConfig(Flags);
    const uploads = yield* Vercel.ReadWriteBlob(Uploads);
    const orders  = yield* Vercel.SendMessage(Orders);     // typed Topic value

    yield* Vercel.subscribe(Orders, Effect.fn(function* (order) {
      yield* uploads.put(`receipts/${order.orderId}.json`, JSON.stringify(order));
    }));
    yield* Vercel.cron("0 3 * * *", nightlyCleanup);

    return {
      fetch: Effect.gen(function* () {
        yield* orders.send({ orderId: crypto.randomUUID(), amountCents: 4200 }).pipe(Effect.orDie);
        return yield* HttpServerResponse.json({ queued: true });
      }),
    };
  }).pipe(Effect.provide([Vercel.ReadEdgeConfigHttp, Vercel.ReadWriteBlobHttp, Vercel.SendMessageHttp])),
) {}

Plain async mode works too (Vercel.Function("Hooks", { main, env }) — web-standard handlers, InferEnv-typed process.env), plus Website.{StaticSite,Nuxt,Astro,SvelteKit} over the same deploy engine.

What's inside

  • Auth: VercelAuth (env / stored token, team picker), VercelEnvironment (teamId scoping), alchemy login integration
  • Deploy engine (internal library): Build Output v3 synthesis → SHA1 upload → immutable deployments with meta.alchemy* stamps; crash recovery by content hash; protection-bypass secrets minted before first deploy (they only open later deployments); state-persisted managedEnv baseline for Vercel's write-only sensitive env rows
  • Compute: Function (async + Effect bridge — re-entrant for Fluid concurrency, per-request Scope settled via waitUntil, SIGTERM instance-scope close), Function.URL, InvokeFunction (circular A↔B topologies via precreate), cron
  • Queues: typed Topic config values (schema + region pinning), SendMessage, poll ReceiveMessages, and subscribe — real platform push delivery through a separate consumer .func (a trigger on the main function would kill all public HTTP routing)
  • Resources: Project, ProjectEnv, EdgeConfig (+EdgeConfigToken), BlobStore (+store↔project connections), Domain, DnsRecord, ProjectDomain, Cert, Webhook, Drain, FirewallConfig, CustomEnvironment, SharedEnv, AccessGroup(+Project), Alias + promoteToProduction/rollbackProduction, RollingRelease
  • Topology: project-per-stage for durable stages; ephemeral/PR stages share a durable stage's project via cross-stage refs as additive preview tenants; hard invariant of one production owner per project

Entitlement-gated surfaces (Enterprise access groups, multi custom envs, certs) ship fully implemented with typed-tag probe tests and VERCEL_TEST_* skipIf gates.

🤖 Generated with Claude Code

…+ Effect), and the platform surface

Infrastructure-as-Effects support for Vercel at parity with the Cloudflare
provider, built and live-tested against a real Pro team (60+ live tests).

- Auth: VercelAuth (env/stored token, team picker), VercelEnvironment
  (teamId scope), providers() barrel, alchemy login integration
- Deploy engine (internal): Build Output v3 synthesis, SHA1 upload-all,
  immutable deployments with meta stamps, crash recovery by content hash,
  protection-bypass minted before first deploy, managedEnv baseline for
  write-only sensitive env rows
- Vercel.Function: async mode (web-standard handlers, InferEnv typed env)
  AND Effect mode (two-phase class, per-request Scope settled via
  waitUntil, cron router, SIGTERM instance-scope close); Function.URL;
  InvokeFunction with circular A<->B precreate support
- Resources: Project, ProjectEnv, EdgeConfig (+EdgeConfigToken),
  BlobStore (+store<->project connections), Domain, DnsRecord,
  ProjectDomain, Cert, Webhook, Drain, FirewallConfig, CustomEnvironment,
  SharedEnv, AccessGroup(+Project), Alias (+promote/rollback actions),
  RollingRelease
- Capabilities: ReadEdgeConfig, ReadBlob/WriteBlob/ReadWriteBlob,
  SendMessage/ReceiveMessages + subscribe (Vercel Queues — typed Topic
  values, real push delivery via a separate consumer .func), cron
- Websites: StaticSite, Nuxt (built-in nitro preset), Astro, SvelteKit
- distilled submodule: binary-body codegen + 31 OpenAPI patches
  (alchemy-run/distilled#457)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rrowing, misc

- InvokeFunction: by-logical-id forward references (Vercel.InvokeRef) so
  circular A<->B pairs need no mutual class import (the type-inference
  cycle collapsed both classes to any); layer-scoped forward-ref
  constructor keeps Providers out of the runtime callable's R
- Blob Read/Write clients narrow impossible-by-construction tags to
  defects, preserving least-privilege error unions
- EdgeConfigToken diff: drop the {}-default poisoning Input inference
- sendMessageFromEnv constrained to service-free schemas
- distilled bump: typed 404 on filterProjectEnvs

Full workspace tsc green; live suite 65 passed / 0 failed; census clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alchemy-version-bot

alchemy-version-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

Alchemy

alchemy

bun add https://pkg.ing/alchemy/0d72e38

@alchemy.run/better-auth

bun add https://pkg.ing/@alchemy.run/better-auth/0d72e38

@alchemy.run/cloudflare-runtime

bun add https://pkg.ing/@alchemy.run/cloudflare-runtime/0d72e38

@alchemy.run/frontend-frameworks

bun add https://pkg.ing/@alchemy.run/frontend-frameworks/0d72e38

@alchemy.run/node-utils

bun add https://pkg.ing/@alchemy.run/node-utils/0d72e38

@alchemy.run/pr-package

bun add https://pkg.ing/@alchemy.run/pr-package/0d72e38

@alchemy.run/floci

bun add https://pkg.ing/@alchemy.run/floci/0d72e38

Distilled

@distilled.cloud/core

bun add https://pkg.ing/@distilled.cloud/core/3401096

@distilled.cloud/aws

bun add https://pkg.ing/@distilled.cloud/aws/3401096

@distilled.cloud/axiom

bun add https://pkg.ing/@distilled.cloud/axiom/3401096

@distilled.cloud/cloudflare

bun add https://pkg.ing/@distilled.cloud/cloudflare/3401096

@distilled.cloud/neon

bun add https://pkg.ing/@distilled.cloud/neon/3401096

@distilled.cloud/planetscale

bun add https://pkg.ing/@distilled.cloud/planetscale/3401096

sam-goodwin and others added 7 commits August 14, 2026 00:54
EffectHttp.toHandled closes its internal per-request scope inline after
the response callback, so a handler's Effect.addFinalizer delayed the
HTTP response by the finalizer's full duration (measured: +3348ms for a
3s finalizer) — contradicting the documented post-response-via-waitUntil
contract. Port the Vercel bridge's scope ejection (scopeDisableClose +
chaining onto the bridge's per-event scope closed under ctx.waitUntil)
into toHandledWebResponse; streaming keeps scopeTransferToStream.
Live-verified: 3367.9ms -> 19.3ms with the finalizer still running
post-response; new FinalizerLatency test pins it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A timed-out test re-ran under the default retry, multiplying wall-clock
by (1 + retries); the external timeout/CI wall then killed the process
(exit 130) before TestEnd was written — the 'runner crash with an empty
log'. Timeouts no longer retry (a timeout consumed the whole budget and
may have left an abandoned fiber; matches the speed doctrine), retry
attempts stream their errors to console+log immediately via a TestRetry
event, and an interrupted run appends a RUN INTERRUPTED trailer naming
in-flight tests. Three subprocess regression tests added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…long tail

- Local providers (alchemy dev): Function (full launcher-matrix shim,
  URL-stable hot restarts, cron timers, waitUntil drain), EdgeConfig
  (exact @vercel/edge-config read protocol), Blob (data-plane emulation
  with etag CAS via the VERCEL_BLOB_API_URL override), Queues (v3
  data-plane broker + CloudEvents push dispatch); .local.test.ts suites
  incl. Alchemy.remote() opt-outs
- Vercel.state(): Blob-backed state store function (shared StateApi
  contract, AES-CTR rows, bearer auth, /version) deployed by our own
  engine; alchemy vercel bootstrap/teardown; live e2e green
- Preview tenancy: stable per-stage aliases, per-deployment tenant env
  with the mandatory project-env conflict check, cross-stage stackRef
  test
- logs/tail wiring over the live NDJSON runtime-log stream
- P2: ProjectMember, ProjectRoutes, BulkRedirects, FeatureFlag,
  MicrofrontendsGroup (billing-gated: $250/mo add-on), Check, Team +
  TeamMember (creation/invites gated), WebAnalytics, AIGateway,
  EdgeCache purge, SandboxDrive/Snapshot
- Engine: precreate stub gates only on project-facing props (fixes the
  Function<->BlobStore cycle 500-loop); managedEnv baseline hardening
- Generated API reference for all 22 Vercel services; distilled bump
  (retry give-up on out-of-patience Retry-After + 19 new patches)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan.make skipped the cold-start Unowned probe whenever news carried
unresolved Outputs (the comment claimed such resources 'cannot be
pre-existing') — but physical identity usually comes from a deterministic
name, so a same-name resource could pre-exist while env/binding inputs
were unresolved, and reconcile silently converged onto it without the
--adopt consent gate. Effect-valued props count as unresolved too, so
Cloudflare/AWS circular-binding env patterns were equally exposed.

The probe now runs for every greenfield row, handing read the
stripUnresolved(news) shape it is already contractually required to
tolerate; probe failures degrade to the pre-fix behavior (best-effort,
mirroring the #995 recovery-read). Three engine tests pin it — the first
two failed pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… data planes

Wave E — 8 interaction chains (test/Vercel/Chains/), each 3-8 live
reconciliation cycles asserting changed-vs-stable, which surfaced and
fixed:
- DnsRecord: silent data loss on parent-domain same-name replacement
- ProjectDomain: same-name re-points are delete-first (one project per
  domain name per team)
- BlobStore: connected-store replacement is delete-first; forceDestroy
  purges non-empty stores through the data plane (409 not_empty had made
  them undeletable); connection races hardened
- Function: URL read-back no longer trusts nondeterministic alias
  ordering; generated project names capped at 35 chars (auto-domain
  truncation collisions); Alias guards precreate stubs with a typed error
- State store: versioned row pathnames (overwritten blobs are eventually
  consistent); tenant flows handle Vercel auto-promoting first deployments
- Platform semantics pinned: rollback sweeps custom aliases; external
  OIDC tokens are always development-scoped

HTTP alignment — every Vercel wire call now rides distilled's generated
services (blob_data, queues_data, edge_config_data): BlobHttp/QueueData/
EdgeConfigRead internals swapped behind byte-compatible client surfaces,
QueueApi.ts deleted, fromEnv async clients migrated, local-emulation env
contracts preserved. Documented exemptions: the hanging runtime-log
stream, calls to the user's own Functions, and local emulator serving.

Permission audit: '## Runtime authorization' JSDoc on every capability
(platform-injected store tokens, ambient per-deployment OIDC, minted
scoped read tokens, protection-bypass secrets). Live-probed verdict:
project-scoped auth tokens cannot reach team-owned Edge Configs, so
WriteEdgeConfig ships as explicit user-supplied-token opt-in — never an
auto-bound management token — with an ungated probe test that fails
loudly if Vercel ever ships scoped write credentials.

Full suite: 122 passed / 0 failed / 15 gated (62 files); census clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design ruling: a custom environment is ~one config row on a project with
no independent life — it belongs as a future `environment?: string` prop
on Function/Website (ensure-on-use, ownership in attrs; environment maps
1:1 to the alchemy stage by default in shared projects), not a standalone
resource competing with alchemy's stage concept. Removed before the draft
merges so no soon-deprecated API ships. SharedEnv (genuinely independent,
team-level) stays. The isolation model and defaulting rules are recorded
in processes/Vercel/DESIGN.md §12a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
29 hand-written pages mirroring the Cloudflare/AWS docs structure:
- 6-part tutorial building one evolving order-intake app (bare Function
  -> storage bindings -> queues + cron -> local dev -> testing ->
  stages/rollback/CI), every snippet mined from the green test suites
- guides: compute (functions, fluid, deployments-and-rollback, cron),
  frontend (websites + nuxt/astro/sveltekit/static), data (blob,
  edge-config), messaging (queues incl. the isolation model, webhooks),
  apis (effect-http-api, invoke-function), networking, security
  (secrets-env, deployment-protection + the runtime-authorization
  table, firewall), observability (logs, drains), landing + setup
- Vercel.state() documented alongside the Cloudflare store in
  state-store/; sidebar + provider tab wired in astro.config.mjs
- ground-truth law held: unshipped surfaces (nextjs, environment:,
  domain:, keepDeployments) are deliberately absent; writers reported
  every composition not literally pinned by a test
- site build green (4299 pages, link + diff-indent checks)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sam-goodwin sam-goodwin added the deploy-website Deploy the website preview for this PR label Aug 15, 2026
@alchemy-version-bot

alchemy-version-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Website Preview Deployed

URL: https://alchemyeffectwebsite-website-pr-1217-3tlvgy45zhoz7h7w.testing-2b2.workers.dev

Built from commit 0d72e38.


This comment updates automatically with each push.

sam-goodwin and others added 4 commits August 15, 2026 02:29
The header tab registry (src/docs-tabs.ts) is separate from the sidebar
config and was missed — Vercel joins the primary slot next to
Cloudflare/AWS, owning /vercel and /providers/vercel prefixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Vercel gets its official simple-icons triangle in the tab bar and
  Reference sidebar (it was the only bare-text tab)
- primary tabs gain a group field (core | clouds | databases) and the
  tab bar renders a subtle vertical divider at each group boundary and
  before More: Core CLI | Cloudflare AWS Vercel | PlanetScale Neon
  Prisma | More

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An alchemy app is any number of Functions and Websites, each owning its
own project — the landing page's opening implied a one-Function limit.
Also retitle the showcase section to 'one file' (matching the design
doc's phrasing it was echoing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Straightforward introduction mirroring the Cloudflare landing: intro
paragraph, setup/tutorial pointer, category link lists with one-line
descriptions, and the 'What are you building?' table. Drops the
defensive 'No git, no build pipeline' section and the inline showcase
(both live where they belong: deployments-and-rollback and
compute/functions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deploy-website Deploy the website preview for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant