Skip to content
This repository was archived by the owner on Oct 31, 2026. It is now read-only.
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
14 changes: 10 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@

- This package manages only the Convex plane: projects, deployments, their environment variables,
and the push of the functions inside `Convex.Deployment`. See `docs/design/convex-plane.md`.
- `Convex.state({ team })` keeps Alchemy state in the `samebase` project of the team, with the
functions of `src/store/convex/` and the `alchemyState` component of `src/store/component/`,
which the store pushes itself. Convex codegen writes `src/store/convex/_generated/` and
`src/store/component/_generated/`. The store and its readers (Samebase) are released and upgraded
- `Convex.state({ team })` keeps Alchemy state in the `samebase` project of the team: the
`alchemyState` component of `src/store/component/` and the functions of
`src/store/functions.ts` (exported as `@samebase/alchemy-convex/state/functions`). The store app
`src/store/convex/` serves them as a seed, which the provider pushes itself. The base app of a
team serves the same exports, and the provider never pushes over a base. Convex codegen writes
`src/store/convex/_generated/` and `src/store/component/_generated/`. The modules of
`src/store/` import with `.js` specifiers, because a base app typechecks them without
`allowImportingTsExtensions`. `STORE_VERSION` is a wire contract: raise it only when a function
that a released client calls changes or goes away, because each raise moves every consumer of a
team at once. The store and its readers (Samebase) are released and upgraded
together; a renamed function is a version bump on both sides. See `docs/design/convex-state.md`.
- The Convex CLI owns typecheck, codegen, bundling, and the code push. `Convex.Deployment` only runs
the installed `convex deploy`. Do not move work of the Convex CLI into this package.
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# Changelog

## 0.14.0 (unreleased)

The package exports the functions of the state store, so that the base app of a team serves the
store next to its own components. `Convex.state()` never pushes its store app over a base. The
store version stays 4, so a consumer on 0.13.0 keeps working and moves when it wants to.

- New function `state:host` answers `"seed"` for the store app of this package and `"base"` for the
base app of a team. A store without it is a seed. No other function, argument, result, or row
changes.
- `STORE_VERSION` rises only when a function that a released client calls changes or goes away.
Each consumer needs the exact number, so only such a change moves every consumer of a team at
once.
- The package exports the store functions as code, and its store app `src/store/convex` uses the
same exports:
- `@samebase/alchemy-convex/state/functions`: `stateFunctions(alchemyState, host)`, the twelve
internal `state:*` functions, and `readerFunctions(alchemyState)`, `reader:authorize` and
`reader:repository`.
- `@samebase/alchemy-convex/state/auth.config`: `authConfig(cloudUrl)`, the auth config that
verifies the read tokens.
- `@samebase/alchemy-convex/state/_generated/component.js`: the `ComponentApi` type, which the
Convex codegen of an app that mounts the component imports.

A base app mounts the component and writes three files; see
[the design note](docs/design/convex-state.md#the-stores-functions-are-exported-and-a-base-is-never-pushed-over-0140).

- `Convex.state()` never pushes its store app over a base, because the push replaces every
function of the deployment. A base that answers an older store version fails the run with
`OutdatedBase` (exported): upgrade `@samebase/alchemy-convex` in the base app and push the base
app. A newer store fails the run as before, from either host.
- `reader:repository` accepts only a token of the deployment's own issuer, so a base that accepts
other auth providers does not widen who can read a repository.

## 0.13.0 (unreleased)

The state project of a team is named `samebase`.
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,16 +185,26 @@ Every run needs the team token in `CONVEX_ACCESS_TOKEN`, or the Convex CLI login
team access token names its team, so `Convex.state()` needs no `team` with one; a `team` slug that
is set must be the slug of that team, checked against its projects, and a token of another team
fails the run. The Convex CLI login needs the slug or the id. The first run of a team creates
the project `samebase` and pushes the store functions to its
the project `samebase` and pushes the store app of this package to its
production deployment with the Convex CLI of the app; a later version of this package pushes a
newer store when it finds an older one. A store that is newer than the package fails the run with
both versions, so the repositories of a team upgrade the package together. Each run makes a
newer store app when it finds an older one. A store that is newer than the package fails the run
with both versions, so the repositories of a team upgrade the package together. Each run makes a
deploy key of that deployment that can only run the store's internal functions, and deletes it at
the end. Rows are in the tables `resources` and `outputs` of the component `alchemyState` in the
dashboard, under the stack name.
The store has no lock: run one deploy of a stage at a time, such as with a CI concurrency group.
The store and its readers (Samebase) are released and upgraded together.

The base app of a team can serve the store in the `samebase` project, next to its own components.
It mounts the component from `@samebase/alchemy-convex/state/convex.config`. It exports the
store functions of `@samebase/alchemy-convex/state/functions`:
`stateFunctions(components.alchemyState, "base")` and `readerFunctions(components.alchemyState)`.
It exports its auth config from `@samebase/alchemy-convex/state/auth.config`. Then
`state:host` answers `"base"`, and `Convex.state()` never pushes over that app: a base
that is older than the package fails the run with `OutdatedBase`, and the base app upgrades its
`@samebase/alchemy-convex`. See
[the design note](docs/design/convex-state.md#the-stores-functions-are-exported-and-a-base-is-never-pushed-over-0140).

Store version 3 supports native subscriptions. A trusted server checks repository access, then
calls internal `reader:authorize({ repository, subject })` with a deployment credential. It gives
the returned token to a separate `ConvexClient` connected to the state deployment. The client
Expand Down
112 changes: 111 additions & 1 deletion docs/design/convex-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,96 @@ the team upgrades a consumer: the move comes before the first upgrade, and no de
This version does not export the component from the package. A later version exports it, so that
other apps can mount it.

## The store's functions are exported, and a base is never pushed over (0.14.0)

A base is the app of a team in the `samebase` project. It holds the team-level components: the
hub, which mounts `@samebase/identity-broker`, and the `alchemyState` component of this package.
The store app of this package, `src/store/convex`, is a seed: the provider pushes it to a project
that has no base. A push replaces every function of a deployment. Thus a base serves the same
fifteen functions as the seed, with the same names and the same results, and the same auth
config. The provider never pushes its store app over a base, because that push erases the hub.

The base app does not copy the functions. The package exports them as code, and the store app of
the package uses the same exports:

- `@samebase/alchemy-convex/state/functions`:
- `stateFunctions(alchemyState, host)`: the thirteen internal functions `version`, `host`,
`stacks`, `stages`, `list`, `get`, `replaced`, `set`, `remove`, `removeStack`, `getOutput`, `setOutput`,
and `readStack`. `alchemyState` is `components.alchemyState` of the app, of the type
`ComponentApi`. `host` is `"seed"` or `"base"`. Each function has the arguments, the result
validator, and the semantics of store version 4. `host` is new.
- `readerFunctions(alchemyState)`: the internal mutation `authorize` and the public query
`repository`.
- `@samebase/alchemy-convex/state/auth.config`: `authConfig(cloudUrl)`, the auth config that
verifies the read tokens of `reader:authorize`.
- `@samebase/alchemy-convex/state/_generated/component.js`: the `ComponentApi` type. The Convex
codegen of an app that mounts the component imports it for `components.alchemyState`.

The exported modules import other modules with `.js` specifiers. A base app typechecks them with
its own Convex tsconfig, which does not set `allowImportingTsExtensions`.

`state:version` answers a number, as before. `state:host` answers `"seed"` or `"base"`. A store
without `state:host` was pushed before 0.14.0, and it is a seed. `Convex.state()` reads
`state:version` on each run, and `state:host` only when the store is older. It does this:

| The store | The run |
| -------------------------------------------- | ----------------------------------------------------------------- |
| No function, and the deployment has no code | Pushes the store app. |
| No function, and the deployment has code | Fails: another app has the name of the store. |
| A seed of an older version | Pushes the store app. |
| A base of an older version | Fails with `OutdatedBase`, which names the dependency to upgrade. |
| The version of the package, from either host | Reads and writes. |
| A newer version, from either host | Fails with both versions, as before. |

The run reads the store again right before a push, because another run can push a newer store, or
the base app, in the meantime. The push has no lock (see Limits).

`STORE_VERSION` is a wire contract, not a release number. Each consumer needs the exact number, so
a raise moves every consumer of a team at once. So it rises only when a function that a released
client calls changes its arguments, its result, or its meaning, or goes away. A new function does
not raise it. 0.14.0 adds `state:host` and changes no function, so the number stays 4: a consumer on
0.13.0 keeps working, also next to a base, and it moves when it wants to.

The base app mounts the component without a name, so that its rows are the rows of the seed:
`app.use(alchemyState)` in `convex.config.ts`, with
`import alchemyState from "@samebase/alchemy-convex/state/convex.config"`. Then it writes three
files. Each file names every export, so that the Convex analysis finds each function.

```ts
// convex/state.ts
export const {
version,
host,
stacks,
stages,
list,
get,
replaced,
set,
remove,
removeStack,
getOutput,
setOutput,
readStack,
} = stateFunctions(components.alchemyState, "base");
// convex/reader.ts
export const { authorize, repository } = readerFunctions(components.alchemyState);
// convex/auth.config.ts
export default authConfig(env.CONVEX_CLOUD_URL);
```

`stateFunctions` and `readerFunctions` come from `@samebase/alchemy-convex/state/functions`,
`authConfig` from `@samebase/alchemy-convex/state/auth.config`, `components` from
`./_generated/api`, and `env` from `./_generated/server`. A base app that has more auth providers
puts the providers of `authConfig(env.CONVEX_CLOUD_URL)` into its own list.

The store version of a base is the store version of the `@samebase/alchemy-convex` that the base
app installs. When the number rises, the base app moves first and each consumer follows. A consumer
of a team fails on a base that is older or newer than the consumer.

The rows do not change, and 0.14.0 pushes nothing to a store of version 4. A base app that replaces a seed mounts the same component name, so the base serves
the rows of the seed.

## Measured

Proof: the public starter `samebase/base-alchemy` on a local branch, with the stack named
Expand Down Expand Up @@ -327,6 +417,21 @@ store was not touched. The probe rows were deleted at the end.
| The rows of store version 3 | A row that store version 3 wrote stayed in the root table `resources`, and `state:stacks` answered `[]`. After the import of the root tables into the component (step 3 above), `state:stacks` and `state:readStack` answered the row again. The Convex import keeps `_id` and `_creationTime`. |
| Not measured | The production store of a team, and the move of its rows. |

### Measured for 0.14.0

Proof on 2026-10-30 in team `nicu` on the development deployment `gallant-skunk-112` of `samebase`,
with the Convex CLI login and `convex` 1.48.0. The production deployment of the store was not
touched. The probe rows were deleted at the end, and the deployment is a seed again.

| Check | Result |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| The seed | `npx convex dev --once` in `src/store`: `state:version` answered `4`, `state:host` `"seed"`, `state:stacks` `[]`. The committed `_generated` files did not change. |
| The reader token | A token of `reader:authorize` read a probe stack through `ConvexHttpClient` with the issuer check in place, on the seed and on the base. The `__redacted__` value was not in the reply. A read without a token and a read with a changed token were refused. |
| The base | A scratch app installed the packed tarball and `convex` 1.48.0, mounted the component, and wrote the three files with host `"base"`. `tsc` with the tsconfig of the Convex codegen passed. `state:version` answered `4`, `state:host` `"base"`. |
| The rows across the hosts | A row that the seed wrote: the base read it, and the seed read it again after its next push. |
| The push of the provider | `convex deploy --typecheck disable --codegen disable` in `node_modules/@samebase/alchemy-convex/src/store` of the scratch app, with a key of `PUSH_KEY_ACTIONS` only and an empty `HOME`, passed. |
| Not measured | The rule of `Convex.state()` on a real base: the provider uses the production deployment, and the unit tests cover the rule. The hub as a base. |

## Limits

- No lock: keep the CI concurrency group for each stage. No encryption beyond Convex at rest; a
Expand All @@ -338,7 +443,12 @@ store was not touched. The probe rows were deleted at the end.
- The push of the store has no lock. Two runs of two package versions that both find an older
store can push in the order new, then old, in the seconds between the second read of the version
and the end of the push; the newer run then fails with both versions, and its next run pushes its
store again. The upgrade happens once for each new package version in a team.
store again. The upgrade happens once for each new package version in a team. A base app that is
pushed in the same seconds is replaced by the store app, and the hub with it, until the next push
of the base app. So a team pushes its base for the first time when no stack of the team runs;
after that the provider never pushes, because the store answers `base`. The base repository pins
one version of this package for its stack and its app, so one bump of that pin upgrades the store
functions and the provider together.
- A team access token (0.6.1): `GET /token_details` names its team, and the dashboard's team list
answers it HTTP 403, so a slug is checked against the `teamSlug` of the team's projects instead;
a team without a project needs the numeric id. Measured with a temporary team token on a
Expand Down
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@samebase/alchemy-convex",
"version": "0.13.0",
"version": "0.14.0",
"description": "The Convex plane for Alchemy v2: projects, deployments, environment variables, and the push of the functions",
"keywords": [
"alchemy",
Expand Down Expand Up @@ -35,6 +35,18 @@
"./state/convex.config": {
"types": "./src/store/component/convex.config.ts",
"default": "./src/store/component/convex.config.ts"
},
"./state/_generated/component.js": {
"types": "./src/store/component/_generated/component.ts",
"default": "./src/store/component/_generated/component.ts"
},
"./state/functions": {
"types": "./src/store/functions.ts",
"default": "./src/store/functions.ts"
},
"./state/auth.config": {
"types": "./src/store/authConfig.ts",
"default": "./src/store/authConfig.ts"
}
},
"publishConfig": {
Expand Down
Loading
Loading