Webiny-js webiny-full-stack-architect
git clone https://github.com/webiny/webiny-js
T=$(mktemp -d) && git clone --depth=1 https://github.com/webiny/webiny-js "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/user-skills/full-stack-architect" ~/.claude/skills/webiny-webiny-js-webiny-full-stack-architect && rm -rf "$T"
skills/user-skills/full-stack-architect/SKILL.mdFull-Stack Extension Skeleton
TL;DR
A full-stack extension bundles API and Admin into a single package with a shared domain layer. The top-level component registers both sides via
<Api.Extension> and <Admin.Extension>, which point to separate entry-point files. Each side follows its own layered architecture pattern — see webiny-api-architect and webiny-admin-architect skills for details.
RULE — Extension Entry Points
Admin extensions CANNOT be directly mounted in
or in any child component tree without going throughwebiny.config.tsx.<Admin.Extension />
The same rule applies to API extensions — they must go through
<Api.Extension />.
These entry-point components are the only way to register code that runs inside the Admin app or the API runtime. They use the
src prop to point to a file that will be loaded in the correct execution environment (browser for Admin, Lambda for API). Bypassing these entry points will fail at runtime because the Admin and API contexts (DI containers, routers, GraphQL registries, etc.) are not available outside their respective runtimes.
YOU MUST include the full file path with the
or .ts
extension in every .tsx
prop. For example, use src
src={"/extensions/lead/src/index.ts"}, NOT src={"/extensions/lead"}. Omitting the file extension will cause a build failure.
YOU MUST use
for the export default
call when the file is targeted directly by an Extension createImplementation()
src prop. Using a named export (export const Foo = SomeFactory.createImplementation(...)) will cause a build failure. Named exports are only valid inside files registered via createFeature.
// CORRECT — always use entry-point components <Api.Extension src={import.meta.dirname + "/api/Extension.js"} /> <Admin.Extension src={import.meta.dirname + "/admin/Extension.js"} /> // WRONG — never mount admin/api code directly <MyAdminComponent /> // Will not have access to Admin DI container <MyApiFeature /> // Will not have access to API DI container
Package Structure
my-extension/ ├── src/ │ ├── index.ts # Single public export │ ├── MyExtension.tsx # Top-level component (registers Api + Admin) │ ├── shared/ # Shared between API and Admin │ │ ├── constants.ts # Model IDs, permission names, etc. │ │ └── types.ts # Shared types │ ├── api/ # API-side code → see webiny-api-architect skill │ │ ├── Extension.ts │ │ ├── domain/ │ │ ├── features/ │ │ └── graphql/ │ └── admin/ # Admin-side code → see webiny-admin-architect skill │ ├── Extension.tsx │ ├── features/ │ └── presentation/
Top-Level Component
The top-level component is the single entry point that consumers use. It registers both the API and Admin extensions:
// src/MyExtension.tsx import React from "react"; import { Api, Admin } from "webiny/extensions"; export const MyExtension = () => { return ( <> {/* API extensions — runs in Lambda */} <Api.Extension src={import.meta.dirname + "/api/Extension.js"} /> {/* Admin extensions — runs in browser */} <Admin.Extension src={import.meta.dirname + "/admin/Extension.js"} /> </> ); };
Conditional rendering can wrap the entry points (e.g., feature flags, config parameters):
<Infra.Env.Is name={"prod"}> <Api.Extension src={import.meta.dirname + "/api/Extension.js"} /> <Admin.Extension src={import.meta.dirname + "/admin/Extension.js"} /> </Infra.Env.Is>
Shared Domain Layer
The
shared/ directory contains types and value objects used by both API and Admin:
// src/shared/constants.ts export const MY_MODEL_ID = "myModel"; // src/shared/MyEntity.ts export interface MyEntityValues { name: string; status: "active" | "inactive"; } export interface MyEntityDto { id: string; values: MyEntityValues; } export class MyEntity { private constructor(private dto: MyEntityDto) {} static from(dto: MyEntityDto) { return new MyEntity(dto); } get id() { return this.dto.id; } get values() { return this.dto.values; } }
Build Parameters
Build parameters pass configuration from
webiny.config.tsx (build time) into both the API runtime and the Admin app. A deployed API must NEVER use process.env to read configuration.
declarations MUST live inside the extension's top-level component, NOT in BuildParam
. Required parameters are exposed as React props on the extension component.webiny.config.tsx
Declaring BuildParams
// src/MyExtension.tsx — declares build params as React props interface MyExtensionProps { apiEndpoint: string; dashboardUrl: string; } export const MyExtension = ({ apiEndpoint, dashboardUrl }: MyExtensionProps) => { return ( <> <Api.BuildParam paramName="MY_API_ENDPOINT" value={apiEndpoint} /> <Admin.BuildParam paramName="DASHBOARD_URL" value={dashboardUrl} /> <Api.Extension src={import.meta.dirname + "/api/Extension.js"} /> <Admin.Extension src={import.meta.dirname + "/admin/Extension.js"} /> </> ); };
Consuming in webiny.config.tsx
webiny.config.tsx// webiny.config.tsx — the ONLY place where process.env is read <MyExtension apiEndpoint={process.env.MY_API_ENDPOINT || ""} dashboardUrl={process.env.DASHBOARD_URL || ""} />
Reading BuildParams
- API side: Inject
via DI — see webiny-api-architect skillBuildParams - Admin side: Use
hook — see webiny-admin-architect skilluseBuildParams()
Checklist
- Create top-level component that uses
and<Api.Extension>
— never mount admin/api code directly<Admin.Extension> - Put shared domain models and constants in
shared/ - Declare
/<Api.BuildParam>
in the top-level component, not in<Admin.BuildParam>webiny.config.tsx - API entry point uses
withcreateFeature
— see webiny-api-architectregister(container) - Admin entry point is a React component with
— see webiny-admin-architect<RegisterFeature> - Use
extensions in all import paths (ESM modules).js
Quick Reference
Entry point: <Api.Extension src={...} /> + <Admin.Extension src={...} /> Shared code: shared/ directory for domain models, constants, types API architecture: → see webiny-api-architect skill Admin architecture: → see webiny-admin-architect skill DI pattern: → see webiny-dependency-injection skill BuildParam declare: <Api.BuildParam paramName="KEY" value={prop} /> <Admin.BuildParam paramName="KEY" value={prop} /> BuildParam read (API): buildParams.get<T>("KEY") via DI (→ webiny-api-architect) BuildParam read (Admin): useBuildParams().get<T>("KEY") (→ webiny-admin-architect) Import extensions: Always use .js extensions in import paths (ESM)
Related Skills
- webiny-api-architect — API-side architecture (features, abstractions, container registration)
- webiny-admin-architect — Admin-side architecture (headless + presentation features)
- webiny-project-structure — Extension registration and
webiny.config.tsx - webiny-dependency-injection — The
DI pattern and injectable servicescreateImplementation