@suluk/platform
v0.19.0
Published
The platform generator (C051): write one `definePlatform` manifest → it plans the shadcn-registry adds, generates the wired Hono entry, and merges each module's provision fragment into a single provision.config. The manifest compiles to a shadcn-add list
Readme
@suluk/platform
Write one manifest; the generator plans the shadcn-registry adds, generates the wired Hono entry, merges each module's
provision fragment, and emits the whole scaffold (package.json / tsconfig / wrangler.toml / .env.example / …). The
higher-level surface over the Suluk registry + @suluk/provision.
There are two authoring surfaces. The legacy one (C051) is a single object and is supported forever. The C053 one splits a platform into a reusable system and a swappable brand, adds typed opts, and lets services compose.
The C053 model
A service is a common interface
defineService<SO, BO>({ id, mount, provision?, deps?, env?, serviceOpts?, brandOpts?, reads?, compose? }) — the shape a
community shadcn registry extends. The 19 core services are exported as typed consts (authService, creditsService, …).
System vs brand
import { defineSystem, defineBrand, definePlatform, authService, creditsService, emailService } from "@suluk/platform";
import { analyticsService } from "@acme/suluk-analytics"; // a community service
export const system = defineSystem({
registry: "MahmoodKhalil57/suluk",
services: [authService, creditsService, emailService, analyticsService],
globalServiceOpts: { ENVIRONMENT: "production", TRUSTED_ORIGINS: "https://app.example" },
serviceOpts: { auth: { mcp: { loginPage: "…", consentPage: "…", resource: "…", scopes: ["credits:read"] } } }, // typed by id
wire: [{ id: "signup-grant", from: "auth.onUserCreated", to: "credits.grantOnSignup", with: { amount: 100 } }],
});
export const brand = defineBrand({
name: "app",
globalBrandOpts: { BRAND_NAME: "App", BASE_URL: "https://app.example", EMAIL_FROM: "[email protected]" },
});
export default definePlatform({ system, brand });A system (services + serviceOpts + globalServiceOpts + wiring) is the reusable, publishable template. A brand
(brandOpts + globalBrandOpts) is thin and swappable — two businesses run the same system with different brands; the generated
entry code is identical, only wrangler.toml [vars] differ.
The 2×2 opts matrix
| | per-service | global |
|---|---|---|
| service axis (how it works) | serviceOpts → the entry (mount opts) | globalServiceOpts (a service reads the keys it needs) |
| brand axis (identity) | brandOpts → [vars] | globalBrandOpts → [vars] |
serviceOpts is typed per service id off the imported service objects (or CoreServiceOptsMap for a string id) — a wrong
opt is a compile error.
Composition (wire)
An edge binds a producer port to a consumer capability. It renders into the producer's existing mount-opt field
(e.g. auth.onUserCreated), not a separate statement — so it reuses a real seam and the hook closure gets a real env.
resolveWiring validates presence, port/capability existence, JSON-safe params, safe identifiers, and acyclicity; fan-out
(several wires on one port) composes in declaration order. A community service participates by offering a capability (fills a
core port) or exposing its own.
CLI
suluk-platform # generate from ./platform.config.ts (legacy OR { system, brand })
suluk-platform --config <path>
suluk-platform migrate # print the { system, brand } split of a legacy config (a starting point)migrate is byte-faithful: liftLegacy → the same generated app as the legacy manifest.
Guarantees
- Byte-identity. A legacy manifest — and any
{ system, brand }with no wire — regenerates the exact bytes the C051 generator produced (pinned by a golden test over the real 18-service reference app). - Fail closed. A missing registry, a wrong-typed wire param, or a colliding wire import throws at generate time, not in the shipped app.
- Own the wiring, npm the logic (C052). A community extends services and composition without forking
@suluklogic.
