Skip to content

feat: self-hosted serve command and admin API URL fix - #5374

Merged
adrians5j merged 124 commits into
nextfrom
adrian/self-hosted-2-2026-07-07
Jul 11, 2026
Merged

feat: self-hosted serve command and admin API URL fix#5374
adrians5j merged 124 commits into
nextfrom
adrian/self-hosted-2-2026-07-07

Conversation

@adrians5j

@adrians5j adrians5j commented Jul 7, 2026

Copy link
Copy Markdown
Member

What changed

The self-hosted (server) flavour gained a real local-dev and production workflow:

  • webiny-server serve — runs built apps as long-running servers: serve api boots the built API HTTP handler, serve admin statically serves the admin SPA (with client-side-routing fallback and correct cache headers), and serve (no app) runs both. Apps must be built first.
  • webiny-server watch api — now boots the API automatically alongside the build watchers, with cleaned-up output: no startup crash before the first build, Node --watch chatter suppressed, and clear api ✔ listening / api ↻ reloaded lines. Port honours a host-injected PORT (e.g. portless), then WEBINY_API_PORT, then a default.
  • Admin API URL fix — in the server flavour the admin app ignored the configured API URL and fell back to its own origin. Root cause: the built-in extension definitions were never registered, so the config hydrator silently dropped Admin.ApiUrl/EnvVar. The server flavour now registers them via <ProjectServer />.
  • Config API parity with AWS@webiny/project-server now exposes the Api, Admin, Cli, Infra, and Project namespaces (minus AWS/Pulumi/deploy/environments concepts, which don't apply), so server projects import config helpers from one place.
  • Removed the unused OnEntryBeforeCreate CMS config extension.
  • webiny-server watch with no app is temporarily disabled (run apps separately) while the combined output is refined.

Changelog

Serve and watch for self-hosted projects, plus admin API URL fix

Self-hosted projects previously couldn't run their built apps and, when watching, the admin area talked to the wrong address. You can now serve the API and admin locally or in production, and the admin reliably uses the API URL you configure.

Squash Merge Commit

feat: add self-hosted serve command and fix admin API URL (#5374)
feat(project-server): serve api/admin and register extensions (#5374)

adrians5j and others added 30 commits June 15, 2026 12:30
…pi-infra-aws

Phase 1 of the api-infra composition layer (see plans/api-infra-composition-layer.md).

The GraphQL handler's composition root — ~30 feature registrations in a specific
order — lived in the scaffolding template
`_templates/appTemplates/api/graphql/src/index.ts`, exercised only by full e2e
deploy. That's why wiring/fresh-install bugs (e.g. "No registration found for
FileModel") surface only in CI e2e, never in a test.

Move it verbatim (order preserved) into a real package, @webiny/api-infra-aws,
behind `createWebinyApiHandler(config)`. The only project-specific input is
`extensions`; everything else is standard AWS/env wiring owned by the package.
The template collapses to a 3-line shim. This makes the comp-root a real,
integration-testable unit (Phase 2 adds the fresh-install test).

No behavior change. Build + adio + lint green; ESM imports validated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WebSocket API Gateway (ApiWebsocket.ts) routes $connect/$disconnect/$default
straight to the GraphQL Lambda (graphql.functions.graphql), and the GraphQL
handler already dispatches WS events (registers WebSocketEventType +
WebSocketLambdaHandler). No pulumi module deploys a separate websockets
function, and nothing references appTemplates/api/websockets — it's a leftover
from before WS was consolidated into the graphql handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Precondition for Phase 2 integration testing: createWebinyApiHandler defaulted
to @webiny/aws-sdk's getDocumentClient() + process.env.DB_TABLE, which can't be
pointed at a test DynamoDB. Add optional config.documentClient / config.dbTable
(defaulting to the AWS client + env) so an integration test can boot the full
handler against a local DynamoDB. No behavior change for the app (template
passes neither → defaults preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…variants

Carve @webiny/api-infra-aws into a storage-agnostic base plus two thin
storage-variant packages, mirroring the api-headless-cms-ddb / -ddb-es
convention (and giving each variant real dependency isolation — the DDB path
never pulls OpenSearch, and vice versa).

- @webiny/api-infra-aws (base): transport + all storage-agnostic API features +
  engine, in the order-sensitive sequence, with two injected hooks —
  registerRootStorage / registerRequestStorage — for the variant's storage.
- @webiny/api-infra-aws-ddb: supplies DynamoDB storage ops (core/CMS/audit-logs/
  ACO/websockets). No OpenSearch deps.
- @webiny/api-infra-aws-ddb-os: OpenSearch core + HeadlessCmsDdbEs CMS storage +
  DbRegistry (per-request, before CMS), DDB for the rest. OpenSearch client
  injectable for tests.

Templates shim to their variant: the default graphql handler → -ddb; the
OpenSearch extension handler → -ddb-os. Single source of truth for the wiring;
the two variants can't drift. Builds + adio + lint green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first test that boots the entire API composition root — createWebinyApiHandler
with every feature registered — against an in-process (dynalite) DynamoDB and
drives a real GraphQL request. Previously the comp root lived only in an app
template, exercised solely by full e2e deploy.

Asserts the base /graphql schema builds and resolves (`{ __typename }` → "Query")
on a fresh/empty DB, with no DI wiring failure. This catches a whole class of
comp-root bugs — a feature that fails to register, a broken base schema, a
registration-order regression — before deploy.

NOTE: this does NOT yet reproduce the CI "No registration found for FileModel"
install failure — the base schema builds fine here because FileManagerAppFeature
register()s FileModel and FmGraphQLSchema is on the base schema. That failure is
on a different path (likely the install-wizard state / a route this query doesn't
build), and is a follow-up to target specifically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s to api-infra-aws

Phase 3: make event-handler-aws a pure, domain-free transport package. The three
loader decorators (ApiGatewayIdentityLoaderDecorator, ApiGatewayTenantLoaderDecorator,
S3TenantLoaderDecorator) were the only thing in event-handler-aws importing
@webiny/api-core (its requestContext RequestIdentityLoader/RequestTenantLoader).

- Move the three decorators (+ the S3 decorator's test) into @webiny/api-infra-aws,
  the composition layer that legitimately depends on both transport and domain.
- ApiGatewayFeature / S3Feature become transport-only (event type + router); the
  base createWebinyApiHandler now registers the ApiGateway loader decorators after
  ApiGatewayFeature (same order: tenant inner, identity outer).
- Drop @webiny/api-core from event-handler-aws entirely (prod + test).

event-handler-aws now has zero api-* dependency. Builds + adio + lint green;
event-handler-aws 4 files/18 tests, api-infra-aws base S3 decorator test 3/3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The standard Webiny packages (event-handler-core, event-handler-aws, api-core)
have no root index.js — in-repo imports resolve via tsconfig paths (→ src) and at
publish "webiny.publishFrom": "dist" makes dist/ the package root, so
main: "index.js" resolves to the built dist/index.js. The hand-written
`export * from "./dist/index.js"` shim is never used; it was mistakenly copied
from event-handler-server (which inherited it from the old event-handler-node).

Remove it from the three api-infra packages. Builds + the -ddb integration test
pass without it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-2026-06-15

# Conflicts:
#	packages/cli-core/package.json
#	packages/cli/files/references.json
#	yarn.lock
`controller.response`/`.state`/`.logger`/`.runtime` are added to api-core's
TaskController via a `declare module` augmentation that lives in
@webiny/background-tasks. Consumers importing TaskDefinition from
`webiny/api/tasks` (e.g. task extensions) only got the bare api-core controller
and failed to typecheck with "Property 'response' does not exist".

Previously the app handler imported @webiny/background-tasks directly, which
incidentally pulled the augmentation into the compile program; the api-infra
handler shim no longer does. Load it explicitly via a side-effect import so the
full controller interface is always available to `webiny/api/tasks` consumers.
The import is preserved in the built .d.ts, so the augmentation propagates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rror the extension instead

packages/webiny/src/api/tasks.ts is GENERATED by `webiny generate-webiny-package`
(source: packages/api-core/src/exports/api/tasks.ts) — a hand-edit there is
overwritten on regen, so it's the wrong place. And moving the augmentation import
into api-core/src/exports would create an api-core → background-tasks cycle
(background-tasks already depends on api-core). Both need a team decision.

For now: revert the generated-file edit and mark the failing `controller.response`
usages in the SelfCleaningTask extension with @ts-expect-error (self-cleaning —
they'll flag once the augmentation is properly fixed). The underlying issue
(TaskController's response/state/logger/runtime live in a background-tasks
`declare module` augmentation that must be in the compile program) is pre-existing
and to be fixed at the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…026-07-03

# Conflicts:
#	packages/cli/files/references.json
Rename the API composition packages to parallel the event-handler-* transport
layer they build on:
- @webiny/api-infra-aws        → @webiny/api-event-handler-aws
- @webiny/api-infra-aws-ddb    → @webiny/api-event-handler-aws-ddb
- @webiny/api-infra-aws-ddb-os → @webiny/api-event-handler-aws-ddb-os

Pure rename: dir moves + package names + all imports + template shims +
comments; regenerated tsconfigs/references/lockfile. All three build.
(api-event-handler-core extraction + template-dep prune follow in separate commits.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…om api template

The graphql handler no longer imports the individual @webiny/api-* packages —
it goes through @webiny/api-event-handler-aws-ddb / -ddb-os, which pull the full
feature + storage set transitively; extensions use the `webiny` meta-package.

Remove the 17 storage/transport-ADAPTER deps (pure wiring, never imported by
extension code, now fully encapsulated by the handler): api-*-ddb / -ddb-es /
-sql / -os storage adapters, api-*-aws transport, api-opensearch,
api-dynamodb-to-elasticsearch, api-background-tasks-*, api-websockets-server.

Kept: the two handler packages, infra (aws-sdk/build-tools/db-dynamodb/handler-*),
and the domain-feature packages (api-core, api-headless-cms, api-aco, etc.) as a
safety margin since extensions may import their types directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…equestStack

Pull the per-request API feature composition out of api-event-handler-aws into a
new transport-agnostic package, @webiny/api-event-handler-core, exposing
`registerApiRequestStack(container, config)`. It registers every domain feature +
the GraphQL engine in the exact (order-sensitive) sequence, ONE place so the
feature set + ordering can't drift between transports.

The three transport-/storage-specific interleave points are injected as hooks at
their exact positions: registerRequestStorage (before CMS), registerRealtimeTransport
(after WebsocketsFeature), registerSchedulerTransport (after SchedulerFeature).

api-event-handler-aws now keeps only the ROOT AWS transport wiring (API Gateway +
auth/tenant loaders, bg-task + WebSocket Lambda invocations, DynamoDB, Cognito,
storage) and delegates its request phase to registerApiRequestStack, passing the
AWS WebSockets + scheduler hooks. 18 domain-feature deps moved from aws → core.

This is what the upcoming @webiny/api-event-handler-server will reuse. Verified:
the aws-ddb dynalite integration test (full handler boot → core) passes unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…over shared core

Add @webiny/api-event-handler-server: a Node HTTP server transport composition
that reuses @webiny/api-event-handler-core's registerApiRequestStack — proving
the request stack is genuinely transport-agnostic. Notably it calls
registerApiRequestStack with NEITHER the realtime nor the scheduler hook (both
are AWS-specific: API Gateway WebSockets / EventBridge Scheduler), validating
that those interleave hooks are correctly optional.

EXPERIMENTAL / not deployable yet — the Node transport in @webiny/event-handler-server
is missing two primitives (documented in the handler header): (1) a routing
terminal that dispatches the translated IHttpRequest through HttpRouter and
translates the response back (AWS has ApiGatewayHttpRouterHandler; Node has only
the translator), and (2) Node auth/tenant loader decorators. Both belong in
event-handler-server, not in this composition layer. No template wires it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…' into adrian/server-flavour-2026-07-03

# Conflicts:
#	packages/cli/files/references.json
#	yarn.lock
…server api template

Add storage variants over the experimental Node server transport base, mirroring
the AWS side:
- @webiny/api-event-handler-server-ddb — DynamoDB storage (uniform DI Features;
  the base bakes in no DB, so the variant registers DbFeature itself).
- @webiny/api-event-handler-server-sql — SQL storage; caller supplies the Knex
  client. Unlike DDB, the SQL storage layer is a MIX (ApiCoreSql/WebsocketsSql are
  DI Features; CMS/ACO/audit-logs SQL are legacy RegisterExtensionPlugins applied
  via registerExtensions), composed to mirror the api-headless-cms-sql test setup.

Wire the server-flavour api graphql template to the SQL variant as a thin shim
(replacing the old plugin-based createHandler placeholder that stalled this branch).

⚠️ Both variants are BUILD-VERIFIED ONLY. They inherit the server transport's
not-yet-deployable status (missing Node routing terminal + auth/tenant loaders),
and no SQL-backed API handler has been runtime-assembled before — this is a
scaffold/starting point. Builds + adio + lint green across all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Not needed for now — the server flavour targets SQL. Remove
@webiny/api-event-handler-server-sql's DDB sibling and the stale comment
references to it. The base + sql variant are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adrians5j adrians5j changed the title adrian/self-hosted-2-2026-07-07 feat: self-hosted serve command and admin API URL fix Jul 9, 2026
@adrians5j

Copy link
Copy Markdown
Member Author

/vitest

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Vitest tests have been initiated (for more information, click here). ✨

Group Status
No storage ✅ 53/53 passed
DDB ✅ 17/17 passed
DDB+OS ✅ 20/20 passed
SQL ✅ 10/10 passed
❌ Failed packages

DDB+OS

  • api-file-manager

@adrians5j
adrians5j marked this pull request as ready for review July 10, 2026 11:16
@adrians5j
adrians5j merged commit 7f0ba69 into next Jul 11, 2026
66 of 67 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant