# 02 · Quickstart

Get one extension installed and rendering, end to end, before designing
anything. Track A creates and ships a UI-only extension (one admin page) from
the authoring scaffold. Track B adds a wasm server route. Both tracks end with
the extension installed on a Cedros site through the normal operator flow.

## Prerequisites

- **Node.js 24.17.0 LTS + npm 11.13.0** — the supported authoring baseline
  (`node --version`, `npm --version`; the example ships a `.node-version`).
  Bun/pnpm/Yarn are fine for private iteration only when the final archive
  still passes the documented Node/npm validation gate.
- **A Cedros site you administer** (a local dev host or a staging site), with
  an admin account that has the ManageExtensions permission.
- **For Track B**: a Rust toolchain with the `wasm32-wasip2` target
  (`rustup target add wasm32-wasip2`), and a host with the wasm extension
  runtime enabled — confirm via
  `GET /admin/runtime/wasm-extensions/capabilities`
  ([`07-wasm-server-backends.md`](07-wasm-server-backends.md)).

## Track A: scaffold, package, install

In Admin, open **Extension builder**, download the versioned extension
authoring kit, and unzip it at the target repository root. Copy its runnable
minimal example instead of fetching a Cedros package from npm:

```sh
cp -R cedros-extension-authoring-kit/docs/extension-authoring/examples/minimal-extension \
  ./my-extension
cd my-extension
npm ci
npm run validate
npm run package
cargo test --manifest-path packages/server/Cargo.toml
```

The checked-in runnable floor still lives at
[`examples/minimal-extension`](examples/minimal-extension/) — a complete
family (server registration crate, React package with one admin page, React Native
bootstrap) with sync/validate/package scripts. Before implementation, replace
the demo identity and package-family names in the canonical manifest and
package manifests. In `packages/server/Cargo.toml`, rename `[package].name` but
leave `[lib].name` omitted so Cargo derives the same underscore-normalized
crate name used by `registration.server.entrypoint`. If an existing project
needs an explicit `[lib].name`, rename it too. Then refresh the npm lockfile;
the validation script rejects package, crate, entrypoint, bootstrap, generated
manifest, or version drift:

```sh
npm install --package-lock-only --ignore-scripts --audit=false --fund=false
npm run sync:manifest
npm ci
npm run validate
```

What just happened:

1. `sync:manifest` generated `cedros-extension.bootstrap.json` and
   `src/generated-manifest.ts` for both React packages from the one canonical
   `cedros-extension.manifest.json` — one manifest source, everywhere
   ([`05-package-family-and-registration.md`](05-package-family-and-registration.md)).
2. `validate` checked the alignment rules intake will enforce: schema/platform
   versions, bootstrap deep-equality, package names/versions matching
   `packageFamily`, registration exports resolving to real compiled files.
3. `package` wrote the archive: canonical `cedros-extension.manifest.json` +
   `cedros-extension.bootstrap.json` at the root, sources under `workspace/`,
   and — because the manifest declares `adminModules[]` — the compiled React
   output under `workspace/react/dist/**`
   ([`22-packaging-and-upload.md`](22-packaging-and-upload.md)).

Now install it: in the Cedros admin, open **Extensions**, choose the local
package upload, and select the ZIP from `dist/`. The host validates the
archive, shows the manifest's identity and requested permissions for review,
and records it in the catalog. Install and enable it.

You should see:

- the extension listed as installed + enabled in Extensions, and
- a **Demo Minimal** section in the admin sidebar (the example's one admin
  page), rendered from the compiled JS inside your ZIP — no host rebuild, no
  restart.

If the page shows "Admin bundle not loaded" instead, the manifest fallback is
rendering — the runtime module didn't pass the matching rule (module
`manifest.moduleId`/`extensionId`/`version` and runtime sections/groups must
exactly match the installed manifest and its navigation projection; see
[`12-admin-pages-and-dashboard-cards.md`](12-admin-pages-and-dashboard-cards.md)
and [`25-troubleshooting-and-faq.md`](25-troubleshooting-and-faq.md)).

## Track B: add a wasm server route

A server backend is Rust compiled to a WebAssembly component that the host
loads capability-gated. The host repo carries a minimal working guest at
`server/tests/fixtures/wasm-extension-example/`; the full contract is
[`07-wasm-server-backends.md`](07-wasm-server-backends.md). The short version:

1. **Vendor the WIT.** Download the extension authoring kit and copy its
   `wit/cedros-extension.wit` into your workspace (or copy the identical
   `server/wit/cedros-extension.wit` from the cedros-data repo), then generate
   bindings with `wit-bindgen`. Pin the version/checksum in
   `AUTHORING-KIT.json` and the capability endpoint.
2. **Implement the world.** Export `routes` (`list-routes` + `handle-route`)
   and `jobs` (`list-jobs` + `run-job`; return an empty job list if you have
   none):

   ```rust
   fn list_routes() -> Vec<RouteSpec> {
       vec![RouteSpec {
           route_id: "cedros-demo-minimal:hello".into(),
           method: "GET".into(),
           path: "/hello".into(),
       }]
   }

   fn handle_route(req: RouteRequest) -> RouteResponse {
       // The host passes the FULL mounted path — strip the mount prefix.
       let mount = "/extensions/cedros-demo-minimal";
       let local = req.path.strip_prefix(mount).unwrap_or(&req.path);
       match local {
           "/hello" => json_response(200, r#"{"hello":"cedros"}"#),
           _ => json_response(404, r#"{"error":"not_found"}"#),
       }
   }
   ```

3. **Declare the route.** Add `"cedros-demo-minimal:hello"` to
   `surfaces.server.routes[]` in the manifest. Declared-but-not-exported ids
   fail install atomically; exported-but-not-declared ids fail binding — keep
   the two lists identical.
4. **Build and package.**

   ```sh
   cargo build --release --target wasm32-wasip2
   cp target/wasm32-wasip2/release/<crate>.wasm cedros-extension.server.wasm
   npm run package   # archive now carries the component at the root
   ```

5. **Install.** Upload through the admin Extensions flow again (bump the
   patch version for a one-click update), or headless:

   ```sh
   curl -X POST "$HOST/admin/runtime/wasm-extensions/install" \
     -H "Authorization: Bearer $ADMIN_TOKEN" \
     --data-binary @dist/cedros-demo-minimal-1.0.1.cedros-extension.zip
   ```

6. **Verify.**

   ```sh
   curl "$HOST/extensions/cedros-demo-minimal/hello"
   # → {"hello":"cedros"}
   ```

The route bound at enable time with a hot-swap — no restart. From here, every
other server capability (database, settings, secrets, providers, tags,
telemetry, egress) uses the same declaration/import/reinstall loop, but not the
same authorization or data policy. Before adding one, complete the
[host-seam decision record](03-planning-an-extension.md#required-host-seam-decision-record).
For admin/private routes, parse `request.context_json`, require authenticated
host identity plus the declared permission (or system-admin), and require
host-verified CSRF for mutations before calling any granted import. Managed
providers keep credentials in Cedros; durable records use host database or
storage; important operator actions use the documented audit/site-brain
channel; paid behavior uses published entitlement contracts; all of them need
explicit disable/update/uninstall behavior.

The copied validator permits those valid declarations. It rejects declared
routes/jobs until `cedros-extension.server.wasm` exists at the workspace root,
and `npm run package` then includes that component in the deterministic ZIP.

## Where to go next

- Planning a real extension: [`03-planning-an-extension.md`](03-planning-an-extension.md).
- The full manifest field reference: [`04-manifest-reference.md`](04-manifest-reference.md).
- A compact but complete assembled example (manifest + admin module + wasm
  readiness + release packet): [`24-worked-example.md`](24-worked-example.md).
- When something doesn't render or 404s: [`25-troubleshooting-and-faq.md`](25-troubleshooting-and-faq.md).
