> ## Documentation Index
> Fetch the complete documentation index at: https://86d.store/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a custom Module

> Scaffold, implement, and publish your own Module with Storefront components, Store Admin pages, and API endpoints.

<Warning>
  **In development.** 86d is being built in the open. Every capability is Experimental until it earns evidence, so check [maturity levels](/docs/resources/versioning) before you rely on anything here.
</Warning>

Every [Feature](/docs/resources/glossary#feature) in 86d is a [Module](/docs/concepts/modules), including every first-party entry in the generated registry. There is no privileged internal API: what you write gets the same treatment as `@86d-store/products`. Your Module can add [Storefront](/docs/concepts/storefront) components, [Store Admin](/docs/concepts/admin) pages, and API endpoints.

Every Module declares exactly one storage kind. Use native Zod with the `col` registry for Relational tables, declared Config keys for settings, or `{ kind: "none" }` when the Module owns no durable rows.

## Scaffold it

<Steps>
  <Step title="Create the package">
    ```bash theme={null}
    86d module create my-feature
    ```

    You get a package and surface scaffold under `modules/my-feature/`:

    ```text theme={null}
    modules/my-feature/
    ├── package.json
    ├── tsconfig.json
    └── src/
        ├── index.ts                 Module factory
        ├── schema.ts                Native storage declaration
        ├── mdx.d.ts                 MDX type declarations
        ├── store/
        │   ├── components/mdx.tsx
        │   └── endpoints/routes.ts
        ├── admin/
        │   ├── components/.gitkeep
        │   └── endpoints/routes.ts
        └── __tests__/
            └── index.test.ts
    ```
  </Step>

  <Step title="Declare storage">
    Every Module declares `storage: { kind: "none" }`, `storage: { kind: "config", ... }`, or `storage: { kind: "relational", ... }`. Relational field names are locked: `tables`, `extends`, `anchors`, and `publishes`.

    ```ts src/schema.ts theme={null}
    import type { ModuleStorageDeclaration } from "@86d-store/core/schema";
    import { col } from "@86d-store/core/schema";
    import { z } from "@86d-store/core/zod";

    export const itemShape = z.object({
      id: z.uuid().register(col, { pk: true }),
      title: z.string().max(200),
      published: z.boolean().default(false),
    });

    export const myFeatureStorage = {
      kind: "relational",
      tables: {
        item: { shape: itemShape },
      },
      publishes: {
        item: {
          version: "1.0.0",
          table: "item",
          columns: ["id", "title", "published"],
        },
      },
    } as const satisfies ModuleStorageDeclaration;
    ```

    Wire `storage: myFeatureStorage` from the Module factory. Do not author legacy `Module.schema` field maps.
  </Step>

  <Step title="Write endpoints">
    Add controllers and endpoints. Store endpoints are public. Admin endpoints sit under `/api/admin/...` and require an authenticated Store Admin session, enforced for you.
  </Step>

  <Step title="Enable it">
    ```bash theme={null}
    86d module enable my-feature
    ```

    This adds `@86d-store/my-feature` to your active [Template](/docs/concepts/templates)'s `config.json`.
  </Step>

  <Step title="Regenerate">
    ```bash theme={null}
    86d generate
    ```

    Your Module is now imported statically, its endpoints are mounted under `/api/`, and its components are in the MDX registry.
  </Step>

  <Step title="Run the tests">
    ```bash theme={null}
    bun test
    ```

    The starter test only checks that the factory returns the expected `id` and `version`. Replace it with coverage of what your Module actually does, including what it does when a dependency is unavailable.
  </Step>
</Steps>

## Entry point

`src/index.ts` exports a factory that returns a `Module` with an explicit storage declaration:

```ts src/index.ts theme={null}
import type { Module, ModuleConfig } from "@86d-store/core/types/module";
import { myFeatureStorage } from "./schema.js";
import { storeEndpoints } from "./store/endpoints/routes.js";
import { adminEndpoints } from "./admin/endpoints/routes.js";

export default function myFeature(
  options: ModuleConfig = {},
): Module {
  return {
    id: "my-feature",
    version: "0.0.1",
    storage: myFeatureStorage,
    options,
    endpoints: {
      store: storeEndpoints,
      admin: adminEndpoints,
    },
  };
}
```

It is a factory rather than a singleton so a Store can pass it options through `moduleOptions` in `config.json`.

Declare every cross-Module edge up front: capabilities you provide or accept (exact or caret SemVer ranges), hooks you define or implement, readers over published views, template data projections, and durable event emits/handlers (exact integer schema versions). Provisioning compiles those edges into one execution graph; missing or incompatible edges fail the build, and request paths perform no Module discovery.

## Where your admin pages appear

Declare a group and your pages land in the Store Admin sidebar. The nine top-level groups are **Catalog**, **Sales**, **Customers**, **Fulfillment**, **Marketing**, **Content**, **Finance**, **Support**, and **System**.

Each has collapsible subgroups, such as Sales → Orders, Cart, Billing. Name one with `subgroup` on an `AdminPage`:

```ts theme={null}
admin: {
  pages: [
    {
      path: "/admin/my-feature",
      component: "MyFeatureList",
      label: "My feature",
      icon: "Star",
      group: "Catalog",
      subgroup: "Products",   // optional, overrides the default mapping
    },
  ],
},
```

## Talking to another Module

Cross-Module work uses typed capabilities for immediate decisions. Cross-Module reads use published column-projected views granted at provision time. Legacy `exports` and `requires` field metadata remains during migration; it does not grant table access.

```ts theme={null}
capabilities: {
  accepts: [acceptCapability(someCapability)],
},
```

The runtime validates contracts at startup. A missing capability fails at boot, not on a shopper request.

## Reaching the database

Database access goes through [`ModuleDataService`](/docs/resources/glossary#moduledataservice) as `ctx.data`. It reaches compiled `mod_<moduleId>` tables and declared Config keys only. There is no raw SQL escape and no JSON fallback. When isolation is enforced, each request transaction enters the Module Postgres role with `SET LOCAL ROLE`.

A Module never imports `@86d-store/db` or an ORM client directly:

```ts theme={null}
init: async (ctx: ModuleContext) => {
  // ctx.data is a ModuleDataService scoped to this module's schema
  const controllers = createControllers(ctx.data);
  return { controllers };
},
```

`@86d-store/core/test-utils` gives you an in-memory data service, so your unit tests need no database at all.

## Publishing to npm

Anyone running 86d can install your Module once it is published. Published packages must ship **compiled `dist/`** (JavaScript + `.d.ts`), not raw `src/`. Workspace development can keep `exports` on `./src` for DX; `publishConfig.exports` must point at `./dist`.

<Steps>
  <Step title="Update package.json">
    Set `"private": false`, pick a version, and write a `description` and `keywords` someone can find. Require:

    * `"files": ["dist", "README.md"]` (exclude tests; never include `src`, `.turbo`, or vitest config)
    * `"publishConfig.exports"` mapping every public entry to `./dist/*.js` and `./dist/*.d.ts`
    * A `build` that runs `tsc` with `rootDir: "src"`, `outDir: "dist"`, `declaration: true`, and copies non-TS assets (for example `.mdx`) into `dist`
  </Step>

  <Step title="Build">
    ```bash theme={null}
    bun run build
    ```

    First-party and scaffolded Modules use `"build": "86d module build"`, which runs `tsc` then copies non-TS assets (for example `.mdx`) into `dist/`. Confirm `dist/` contains `.js` and `.d.ts` for every export, and that `npm pack --dry-run` lists only `dist` + README + package metadata. Add `"86d"` as a `devDependency` so the CLI bin resolves.
  </Step>

  <Step title="Rewrite installable dependency specs">
    Replace any `workspace:*` or `catalog:` dependency with a real semver before publish. First-party Releases run `bun run prepare-publish` for this; third-party authors must do it themselves.
  </Step>

  <Step title="Publish">
    ```bash theme={null}
    npm publish --access public
    ```

    First-party Modules ship through Changesets with npm provenance (`bun run release`). If you publish your own, that release and security process is yours to run.
  </Step>
</Steps>

Then anyone installs it with:

```bash theme={null}
86d module add npm:@your-scope/your-module
```

Publishing puts your package on npm. It does not add it to the first-party registry, and it says nothing about compatibility or maturity. Anyone installing it should read it first, and so should you.

## Related pages

* [Add a Module](/docs/guides/adding-modules)
* [How Modules package Store capabilities](/docs/concepts/modules)
* [Storefront](/docs/concepts/storefront)
* [Store Admin](/docs/concepts/admin)
* [Test the Store Runtime](/docs/operations/testing)
* [Versioning and maturity](/docs/resources/versioning)
