Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .changeset/storage-slot-canonical-rename.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
---
"@objectstack/spec": minor
"@objectstack/service-storage": patch
"@objectstack/runtime": patch
"@objectstack/metadata-protocol": patch
"@objectstack/cli": patch
"@objectstack/plugin-email": patch
"@objectstack/plugin-dev": patch
---

feat(spec): `storage` becomes the canonical `CoreServiceName` slot; `file-storage` stays a deprecated v17 alias (#9683)

<!-- adr-0087: not-required (no-migration-prescription) A service-registry slot
name is not authorable metadata — nothing in a stack definition spells it — so
there is no conversion-layer entry to register. Compatibility is carried by the
enum keeping the old member and by @objectstack/service-storage registering the
same instance under both names; the alias retires through the standard
retirement flow at the next major. -->

Maintainer ruling, 2026-08-18, verbatim: 「9683 file-storage 可以叫 storage」.
The `file-storage` slot was the only `CoreServiceName` member whose spelling
diverged from its documented accessor (`services.storage`), with no recorded
reason anywhere in the tree.

- `CoreServiceName` gains `storage` as the canonical member; `file-storage`
stays an accepted, deprecated alias within v17 (it is a published enum
member — existing `getService('file-storage')` callers keep working).
`CORE_SERVICE_PROVIDER` and `ServiceRequirementDef` carry both.
- `@objectstack/service-storage` registers the **same instance** under both
names (the `http.server` / `http-server` pattern), pinned by an
alias-equivalence test.
- Every internal consumer resolves `storage`: the HTTP dispatcher, the email
plugin's attachment store, and `os migrate files-to-references`. Discovery
reports the service under the canonical `storage` key and mirrors the row
verbatim under the `file-storage` key for the alias's v17 lifetime, so
existing discovery readers (e.g. the console endpoint catalog) keep
working.
- Docs (`kernel/runtime-services`, `kernel/contracts`) now document the
canonical slot; a custom v17 provider for this slot should register both
names.
3 changes: 2 additions & 1 deletion content/docs/api/plugin-endpoints.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,7 +179,8 @@ the protocol above and never touched them.

Both routes are retired. `/storage` is service-storage's surface — install that
package to get it. Discovery advertises the route only when the occupant of the
`file-storage` slot actually mounts HTTP handlers, so an in-memory dev
`storage` slot (spelled `file-storage` before #9683; that spelling stays a
deprecated v17 alias) actually mounts HTTP handlers, so an in-memory dev
implementation now reports `handlerReady: false` and no `routes.storage`
instead of pointing at a path with nothing behind it.
</Callout>
Expand Down
4 changes: 3 additions & 1 deletion content/docs/kernel/contracts/storage-service.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,9 @@ The Storage Service provides a unified interface for **file management** — upl

<Callout type="info">
**Source:** `packages/spec/src/contracts/storage-service.ts`
**Service name:** `'file-storage'` — resolve via `kernel.getService('file-storage')`.
**Service name:** `'storage'` — resolve via `kernel.getService('storage')`. The
pre-rename spelling `'file-storage'` stays accepted as a deprecated alias within v17
(#9683); both names resolve the same instance.
</Callout>

---
Expand Down
10 changes: 5 additions & 5 deletions content/docs/kernel/runtime-services/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,11 +16,11 @@ framework today. Managed runtimes provide the `services.*` binding directly.
**`services.*` is the accessor spelling, not necessarily the registry slot.** The name
after `services.` is what this chapter documents each surface under; the string you pass
to `ctx.getService(...)` is the slot the implementation is registered by. They are the
same word for every service below **except storage**, whose slot is `file-storage`
(there is no `storage` alias — see
[`services.storage`](/docs/kernel/runtime-services/storage-service)). Each page states
its own slot in a **Registry slot** bullet; `scripts/check-runtime-services-index.mjs`
holds those to a real `registerService` call.
same word for every service below storage included, since the #9683 rename made
`storage` the canonical slot (its pre-rename spelling `file-storage` stays a deprecated
v17 alias — see [`services.storage`](/docs/kernel/runtime-services/storage-service)).
Each page states its own slot in a **Registry slot** bullet;
`scripts/check-runtime-services-index.mjs` holds those to a real `registerService` call.
</Callout>

This chapter documents the runtime `services.*` APIs used in hook/action/flow/plugin code:
Expand Down
48 changes: 23 additions & 25 deletions content/docs/kernel/runtime-services/storage-service.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,45 +7,43 @@ description: File/object storage contract for upload/download and presigned URL

- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/storage-service.ts`
- **Registry slot:** `file-storage` — **not** `storage`; resolve with
`ctx.getService('file-storage')`. See [Accessor name vs registry
slot](#accessor-name-vs-registry-slot).
- **Registry slot:** `storage` — resolve with `ctx.getService('storage')`.
The pre-rename spelling `file-storage` stays accepted as a deprecated alias
within v17. See [Registry slot and its deprecated
alias](#registry-slot-and-its-deprecated-alias).

## Accessor name vs registry slot
## Registry slot and its deprecated alias

`services.storage` is this chapter's **accessor spelling** — the name the contract surface
is documented under. The **registry slot** is the string the kernel actually keys the
implementation by, and for storage the two differ:
`storage` is the canonical registry slot (maintainer ruling, 2026-08-18, issue
#9683): it is a member of `CoreServiceName`
(`packages/spec/src/system/core-services.zod.ts`), `CORE_SERVICE_PROVIDER` maps
it to `@objectstack/service-storage`, and it is the key the `/api/v1/discovery`
document reports this service's availability under. The accessor spelling and
the slot are now the same word, like every other service in this chapter.

| | value |
|:--|:--|
| Documented accessor | `services.storage` |
| Registry slot | `file-storage` |
| Resolve with | `ctx.getService('file-storage')` |

This is the **only** service in this chapter where they differ; for the other seven the
accessor and the slot are the same word. There is no `storage` alias — nothing calls
`registerService('storage', ...)` anywhere in the platform — so resolving the accessor
spelling throws rather than returning an empty value:
The slot was spelled `file-storage` before the rename, and that spelling is a
published `CoreServiceName` member — so it stays accepted as a **deprecated
alias** within v17. `@objectstack/service-storage` registers the **same
instance** under both names:

```ts
ctx.getService('storage'); // ✗ throws: [Kernel] Service 'storage' not found
ctx.getService('file-storage'); // ✓ the IStorageService documented below
ctx.getService('storage'); // ✓ canonical — the IStorageService documented below
ctx.getService('file-storage'); // ✓ deprecated v17 alias — the SAME instance
```

`file-storage` is the canonical spelling, not an implementation detail: it is the member
listed in `CoreServiceName` (`packages/spec/src/system/core-services.zod.ts`),
`CORE_SERVICE_PROVIDER` maps it to `@objectstack/service-storage`, and it is the key the
`/api/v1/discovery` document reports this service's availability under.
New code resolves `storage`. The alias is retired through the standard
retirement flow at the next major. If you ship a **custom** provider for this
slot in v17, register it under both names the way `service-storage` does, so
callers of either spelling keep resolving it.

<Callout type="warn" title="Not this: the client SDK storage accessor">
A second `storage` accessor exists and is easy to reach for by mistake.
`ObjectStackClient.storage` (`packages/client/src/index.ts`) is the **browser/HTTP client**
surface — `upload(file, scope)`, `getDownloadUrl(fileId)`, `getPresignedUrl(req)`,
`initChunkedUpload(req)` — which calls `/api/v1/storage` over the wire. It is a different
shape from the server-side `IStorageService` documented on this page, which takes storage
**keys** and returns `Buffer`s. Neither one is reachable through
`ctx.getService('storage')`.
**keys** and returns `Buffer`s. `ctx.getService('storage')` resolves the server-side
service documented here — never the client accessor.
</Callout>

## Core Methods
Expand Down
8 changes: 4 additions & 4 deletions content/docs/kernel/services-checklist.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ The ObjectStack protocol defines **15 kernel services** registered via the `Core
│ Plugin Layer │
│ All other services: analytics, auth, automation, │
│ ui, realtime, notification, ai, i18n, │
│ search, file-storage, cache, queue, job │
│ search, storage, cache, queue, job
│ │
│ Discovery API reports availability per service │
│ (available / degraded / stub / unavailable) so │
Expand All@@ -75,7 +75,7 @@ The ObjectStack protocol defines **15 kernel services** registered via the `Core
| 8 | **notification** | `optional` | 7 | ❌ Plugin Required | `@objectstack/service-messaging` |
| 9 | **ai** | `optional` | — | ❌ Nothing ships in this repo | `@objectstack/service-ai` (Cloud/EE — not installable, so the table entry is `null`) |
| 10 | **i18n** | `core` | 3 | ✅ Built-in (in-memory fallback) | `@objectstack/service-i18n` |
| 11 | **file-storage** | `optional` | — | ❌ Plugin Required | `@objectstack/service-storage` |
| 11 | **storage** (deprecated v17 alias: `file-storage`, #9683) | `optional` | — | ❌ Plugin Required | `@objectstack/service-storage` |
| 12 | **search** | `optional` | — | ❌ Nothing ships | — |
| 13 | **cache** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-cache` |
| 14 | **queue** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-queue` |
Expand DownExpand Up@@ -480,11 +480,11 @@ AppPlugin will:

## 11–15. Infrastructure Services

`cache`, `queue`, and `job` are `core` services: like `i18n`, the kernel auto-injects an in-memory fallback when no plugin registers them (see `CORE_FALLBACK_FACTORIES` in `packages/core/src/fallbacks/`). The `optional` services (`file-storage`, `search`) stay disabled until a plugin provides them.
`cache`, `queue`, and `job` are `core` services: like `i18n`, the kernel auto-injects an in-memory fallback when no plugin registers them (see `CORE_FALLBACK_FACTORIES` in `packages/core/src/fallbacks/`). The `optional` services (`storage`, `search`) stay disabled until a plugin provides them.

| Service | Description |
|:--------|:------------|
| **file-storage** | Unified upload/download/delete via `@objectstack/service-storage`, which mounts `/api/v1/storage` itself. Adapters: local FS and S3 (the S3 adapter's `endpoint` + path-style options cover S3-compatible services such as MinIO and R2). |
| **storage** (deprecated v17 alias: `file-storage`, #9683) | Unified upload/download/delete via `@objectstack/service-storage`, which mounts `/api/v1/storage` itself. Adapters: local FS and S3 (the S3 adapter's `endpoint` + path-style options cover S3-compatible services such as MinIO and R2). |
| **search** | **Nothing ships.** `ISearchService` and the engine enum (`elasticsearch`, `meilisearch`, …) exist in `@objectstack/spec`, but no package implements the contract or registers the `search` slot, so `CORE_SERVICE_PROVIDER.search` is `null`. |
| **cache** | General-purpose cache. In-memory fallback; memory or Redis adapter via `@objectstack/service-cache`. |
| **queue** | Message queue. In-memory fallback; durable DB-backed adapter (`sys_job_queue`) via `@objectstack/service-queue` (no BullMQ/Redis adapter is shipped). |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/dispatcher.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@ const result = DispatcherConfigSchema.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **routes** | `{ prefix: string; service: Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| …>; authRequired: boolean; criticality: Enum<'required' \| 'core' \| 'optional'>; … }[]` | ✅ | Route-to-service mappings |
| **routes** | `{ prefix: string; service: Enum<'metadata' \| 'data' \| 'auth' \| 'storage' \| 'file-storage' \| 'search' \| 'cache' \| …>; authRequired: boolean; criticality: Enum<'required' \| 'core' \| 'optional'>; … }[]` | ✅ | Route-to-service mappings |
| **fallback** | `Enum<'404' \| 'proxy' \| 'custom'>` | optional (default: `"404"`) | Behavior when no route matches |
| **proxyTarget** | `string` | optional | Proxy target URL when fallback is "proxy" |

Expand DownExpand Up@@ -83,7 +83,7 @@ Route-resolution failure mode emitted in `error.code`
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **prefix** | `string` | ✅ | URL path prefix for routing (e.g. /api/v1/data) |
| **service** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | Target core service name |
| **service** | `Enum<'metadata' \| 'data' \| 'auth' \| 'storage' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | Target core service name |
| **authRequired** | `boolean` | optional (default: `true`) | Whether authentication is required |
| **criticality** | `Enum<'required' \| 'core' \| 'optional'>` | optional (default: `"optional"`) | Service criticality level for unavailability handling |
| **permissions** | `string[]` | optional | Required permissions for this route namespace |
Expand Down
5 changes: 3 additions & 2 deletions content/docs/references/system/core-services.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ const result = CoreServiceName.parse(data);
* `metadata`
* `data`
* `auth`
* `storage`
* `file-storage`
* `search`
* `cache`
Expand DownExpand Up@@ -65,7 +66,7 @@ const result = CoreServiceName.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | |
| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'storage' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | |
| **enabled** | `boolean` | ✅ | |
| **status** | `Enum<'running' \| 'stopped' \| 'degraded' \| 'initializing'>` | ✅ | |
| **version** | `string` | optional | |
Expand All@@ -82,7 +83,7 @@ const result = CoreServiceName.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | |
| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | |
| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'storage' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | |
| **options** | `Record<string, any>` | optional | |


Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/migrate/files-to-references.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -194,7 +194,9 @@ export default class MigrateFilesToReferences extends Command {
}
const getStorage = () => {
try {
return stack.kernel.getService('file-storage');
// Canonical slot since #9683 (service-storage also registers the
// deprecated `file-storage` alias with the same instance in v17).
return stack.kernel.getService('storage');
} catch {
return null;
}
Expand Down
Loading
Loading