Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a07b12b
feat(realtime): add realtime-handler resource and CLI commands
ImriKochWix Jun 30, 2026
d800c64
fix(lint): apply biome formatting and unused import fixes
ImriKochWix Jun 30, 2026
6703e73
fix(realtime): create handler inside base44/ dir, not project root
ImriKochWix Jun 30, 2026
bbfc700
fix(realtime): scaffold imports RealtimeHandler from @base44/sdk
ImriKochWix Jun 30, 2026
1c78357
fix(realtime): scaffold includes State/Message generic type parameters
ImriKochWix Jun 30, 2026
4789b62
feat(types): auto-generate RealtimeHandlerRegistry from schema.jsonc
ImriKochWix Jun 30, 2026
39ec1cb
fix(types): detect SDK package name and use module context in types.d.ts
ImriKochWix Jun 30, 2026
3512a4d
fix(lint): resolve Biome errors in realtime handler types
ImriKochWix Jun 30, 2026
3a0c923
fix(realtime): use /realtime-handlers endpoint for handler deploy
ImriKochWix Jun 30, 2026
ef7c8f9
fix(types): compile realtime messages as a named catalog, drop the regex
ImriKochWix Jul 5, 2026
5efe4c3
feat(types)!: rename realtime schema sections inbound/outbound -> toC…
ImriKochWix Jul 5, 2026
b9f9391
refactor(cli): rename realtime -> actor (RealtimeHandler -> Actor)
ImriKochWix Jul 9, 2026
2c9337e
feat(types): emit declare module for base44:runtime/actors
ImriKochWix Jul 26, 2026
e68d4ce
refactor(types): base44:runtime/actors re-exports only Actor
ImriKochWix Jul 27, 2026
3a07dc3
feat(actor): scaffold imports Actor from base44:runtime/actors
ImriKochWix Jul 28, 2026
529e202
feat(actor): regenerate types after `actor new` so the scaffolded bas…
ImriKochWix Jul 28, 2026
23d6b6e
fix(actor): scaffold matches the SDK Actor API
ImriKochWix Jul 30, 2026
5a8b43b
fix(actor): make base44:runtime/actors actually resolve in the editor
ImriKochWix Jul 30, 2026
3a63ca2
feat(actor): scaffold schema.jsonc and type the actor from ActorRegistry
ImriKochWix Jul 30, 2026
901f1c8
fix(actor): scaffold a default export — the deploy bundler needs it
ImriKochWix Jul 30, 2026
f1f8ac4
remove scaffolding, will rely on a skill
talge-a11y Aug 10, 2026
04d7f70
minor adjustments
talge-a11y Aug 17, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Actors (realtime handlers): deploy from `base44/actors/` via `base44 actors deploy`, remove with `base44 actors delete`, included in unified `base44 deploy`; `base44 types generate` emits `ActorNameRegistry`. Actor names are validated locally against the server's rule (a JavaScript identifier — no nesting, no dots), and a name shared with a backend function is rejected up front.
- App visibility: `base44 visibility <public|private|workspace>` sets it on the server directly (accepts `--app-id` to target any app). Also configurable via `"visibility"` in `config.jsonc`, which `base44 deploy` applies. New projects scaffold `"visibility": "public"`.
- `base44 build` runs the site's `buildCommand` with `VITE_BASE44_APP_ID` injected, so built bundles always carry the linked app's id.
- `base44 deploy` (and `base44 site deploy`) can now build first: interactive runs ask, and `--build` / `--no-build` pre-answer the prompt.
Expand Down
2 changes: 2 additions & 0 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ CLIError (abstract base class)
└── SystemError (something broke - needs investigation)
├── ApiError # HTTP/network failures
├── ResourceDeployError # One or more sequential deploy items failed
├── FileNotFoundError # File doesn't exist
├── FileReadError # Can't read file
└── InternalError # Unexpected errors
Expand Down Expand Up @@ -110,6 +111,7 @@ See [api-patterns.md](api-patterns.md) for the full `ApiError.fromHttpError()` p
| `SCHEMA_INVALID` | `SchemaValidationError` | Zod validation failed |
| `INVALID_INPUT` | `InvalidInputError` | User provided invalid input |
| `API_ERROR` | `ApiError` | API request failed |
| `RESOURCE_DEPLOY_FAILED` | `ResourceDeployError` | One or more functions or actors failed to deploy |
| `FILE_NOT_FOUND` | `FileNotFoundError` | File doesn't exist |
| `FILE_READ_ERROR` | `FileReadError` | Can't read/write file |
| `INTERNAL_ERROR` | `InternalError` | Unexpected error |
Expand Down
39 changes: 32 additions & 7 deletions docs/resources.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Working with Resources

**Keywords:** resource, entity, function, agent, agent skill, connector, push, readAll, deploy, site, tar.gz, deployAll, ProjectData
**Keywords:** resource, entity, function, actor, agent, agent skill, connector, push, readAll, deploy, site, tar.gz, deployAll, ProjectData

Resources are project-specific collections (entities, functions, agents, agent skills, connectors) that can be read from the filesystem and pushed to the Base44 API.
Resources are project-specific collections (entities, functions, actors, agents, agent skills, connectors) that can be read from the filesystem and pushed to the Base44 API.

## Resource Interface

Expand Down Expand Up @@ -85,6 +85,27 @@ Deploy ships file contents verbatim — the source is never parsed or linted —

Entry files may also import `secrets` and `waitUntil` from `base44:runtime`. Locally, `base44 dev` runs functions on workerd via Miniflare by default — each function is bundled with esbuild + `@deno/loader` (`src/cli/dev/dev-server/function-bundler.ts`), with `base44:runtime` served as a virtual module, secrets as real Worker env bindings and `waitUntil` riding `ctx.waitUntil`. A fallback runtime covers installations where workerd is unavailable (compiled binaries, `B44_DEV_FUNCTIONS_RUNTIME=deno`) and supplies `base44:runtime` via an import map. A project-level `deno.json` import map is not applied to functions — locally or deployed — since only files under `base44/` are uploaded. See [`packages/cli/backend-runtime/README.md`](../packages/cli/backend-runtime/README.md) for the local implementation and its intentional differences from production.

## Actors (project layout)

Actors are stateful realtime handlers, read from the project's actors directory (`base44/actors/`, or `actorsDir` in `config.jsonc`). Discovery is zero-config only: a folder containing `entry.ts` (or `entry.js`) is an actor, and the folder name is the actor name (`actors/ChatRoom/entry.ts` → `ChatRoom`). All `**/*.{js,ts,json,jsonc}` files under that folder are included in the deploy payload, sent via `PUT /api/apps/{app_id}/actors/{name}`. The entry file must default-export the actor class — the deploy bundler imports the default export.

**Naming is enforced locally, mirroring the server.** An actor name becomes a Durable Object class and the WebSocket connect handler, so it must be a plain ASCII JavaScript identifier: `[A-Za-z_][A-Za-z0-9_]*`, max 128 characters, not a JS reserved word, PascalCase by convention. Two consequences differ from functions:

- **Actors cannot be nested.** `actors/games/Arena/entry.ts` is an error, not an actor named `games/Arena`. Since every `entry.{js,ts}` under the actors root is an entry file at any depth, this is also what a helper accidentally named `entry.ts` reports — the error hints at both causes.
- **Folders with a dot in the name are skipped**, using the same `ENTRY_IGNORE_DOT_PATHS` exclusion as functions. A dotted name can never be valid, so `actors/ChatRoom.bak/` is treated as scratch rather than a deploy that would 422.

Both checks run in `readAllActors` (before any upload), and `readProjectConfig` additionally rejects a name shared with a backend function — actors deploy onto the same server-side namespace.

Deliberate gaps (vs functions): no `base44/shared/` inclusion, no `--force` prune, no `list`/`pull`, no plugin actors, and no local `base44 dev` runtime. Authoring guidance (scaffolding, message typing, editor setup for the `base44:runtime/actors` virtual module) lives in the realtime skill, not the CLI. Type generation only emits `ActorNameRegistry` (actor names) into `types.d.ts`.

```bash
base44 actors deploy # Deploy all actors
base44 actors deploy ChatRoom # Deploy specific actors by name
base44 actors delete ChatRoom # Tear down a deployed actor
```

`actors delete` calls `DELETE /api/apps/{app_id}/actors/{name}`, which destroys the published script — it is not a local operation and does not need the actor to still exist on disk. A 404 is reported as "not found" rather than an error, so re-running it is safe.

## Agent skills

Agent skills are app-scoped instruction snippets shared across the app's agents. Unlike other resources they are stored as one markdown file per skill under the agent-skills directory (`base44/agent-skills/`, or `agentSkillsDir` in `config.jsonc`): the filename (without `.md`) is the skill name, the frontmatter `description` is the summary, and the body is the instruction text. Agents reference skills by name via `selected_skill_names`; `selected_workspace_skill_ids` (org-shared workspace skills) is not managed here and is passed through pull/push/deploy untouched.
Expand Down Expand Up @@ -135,11 +156,15 @@ const { appUrl } = await deployAll(projectData);

What it deploys (in order):
1. Entities (via `entityResource.push()`)
2. Functions (via `functionResource.push()`)
3. Agent skills (via `agentSkillResource.push()`)
4. Agents (via `agentResource.push()`)
5. Connectors (via `pushConnectors()`) -- may return OAuth redirect URLs
6. Site (if `site.outputDirectory` is configured) — the legacy tar.gz upload. The env-gated deployments-API lane is reachable only from `base44 site deploy`, not from here (see [deployments.md](deployments.md)).
2. Functions (via `deployFunctionsSequentially()`)
3. Actors (via `deployActorsSequentially()`)
4. Agent skills (via `agentSkillResource.push()`)
5. Agents (via `agentResource.push()`)
6. Auth config (via `authConfigResource.push()`)
7. Connectors (via `pushConnectors()`) -- may return OAuth redirect URLs
8. Site (if `site.outputDirectory` is configured) — the legacy tar.gz upload. The env-gated deployments-API lane is reachable only from `base44 site deploy`, not from here (see [deployments.md](deployments.md)).

Functions and actors deploy sequentially within their resource batch. If any item fails, the command reports every failed item as a structured `ResourceDeployError`, exits non-zero, and does not continue to later resource types.

```bash
base44 deploy # With confirmation prompt
Expand Down
15 changes: 15 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,21 @@ t.api.mockFunctionsPush({ deployed: ["handler"], deleted: [], errors: null });
t.api.mockFunctionsPushError({ status: 400, body: { error: "Invalid" } });
```

### Actor Mocks

```typescript
t.api.mockSingleActorDeploy({ status: "deployed" });
t.api.mockSingleActorDeployError({ status: 400, body: { error: "Invalid" } });
t.api.mockSingleActorDelete();
t.api.mockSingleActorDeleteError({ status: 404, body: { error: "Not found" } });

// Successful requests are captured for payload assertions.
expect(t.api.actorDeployRequests[0]).toMatchObject({
name: "ChatRoom",
entry: "entry.ts",
});
```

### Agent Mocks

```typescript
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ The CLI will guide you through project setup. For step-by-step tutorials, see th
| [`login`](https://docs.base44.com/developers/references/cli/commands/login) | Authenticate with Base44 |
| [`logout`](https://docs.base44.com/developers/references/cli/commands/logout) | Sign out and clear stored credentials |
| [`whoami`](https://docs.base44.com/developers/references/cli/commands/whoami) | Display the current authenticated user |
| `actors deploy` | Deploy local actors to Base44 |
| `actors delete` | Delete deployed actors from Base44 |
| [`agents pull`](https://docs.base44.com/developers/references/cli/commands/agents-pull) | Pull agents from Base44 to local files |
| [`agents push`](https://docs.base44.com/developers/references/cli/commands/agents-push) | Push local agents to Base44 |
| [`connectors initiate`](https://docs.base44.com/developers/references/cli/commands/connectors-initiate) | Initialize a connector on an app and start its OAuth flow |
Expand Down
61 changes: 61 additions & 0 deletions packages/cli/src/cli/commands/actors/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, parseNames } from "@/cli/utils/index.js";
import { ApiError } from "@/core/errors.js";
import { deleteSingleActor } from "@/core/resources/actor/api.js";

async function deleteActorsAction(
{ runTask }: CLIContext,
names: string[],
): Promise<RunCommandResult> {
let deleted = 0;
let notFound = 0;
let errors = 0;

for (const name of names) {
try {
await runTask(`Deleting ${name}...`, () => deleteSingleActor(name), {
successMessage: `${name} deleted`,
errorMessage: `Failed to delete ${name}`,
});
deleted++;
} catch (error) {
if (error instanceof ApiError && error.statusCode === 404) {
notFound++;
} else {
errors++;
}
}
}

if (names.length === 1) {
if (deleted) return { outroMessage: `Actor "${names[0]}" deleted` };
if (notFound) return { outroMessage: `Actor "${names[0]}" not found` };
return { outroMessage: `Failed to delete "${names[0]}"` };
}

const total = names.length;
const parts: string[] = [];
if (deleted > 0) parts.push(`${deleted}/${total} deleted`);
if (notFound > 0) parts.push(`${notFound} not found`);
if (errors > 0) parts.push(`${errors} error${errors !== 1 ? "s" : ""}`);
return { outroMessage: parts.join(", ") };
}

function validateNames(command: Command): void {
const names = parseNames(command.args);
if (names.length === 0) {
command.error("At least one actor name is required");
}
}

export function getDeleteCommand(): Command {
return new Base44Command("delete")
.description("Delete deployed actors")
.argument("<names...>", "Actor names to delete")
.hook("preAction", validateNames)
.action(async (ctx: CLIContext, rawNames: string[]) => {
const names = parseNames(rawNames);
return deleteActorsAction(ctx, names);
});
}
79 changes: 79 additions & 0 deletions packages/cli/src/cli/commands/actors/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import {
Base44Command,
buildDeploySummary,
formatDeployResult,
parseNames,
theme,
} from "@/cli/utils/index.js";
import { InvalidInputError } from "@/core/errors.js";
import { readProjectConfig } from "@/core/index.js";
import { deployActorsSequentially } from "@/core/resources/actor/deploy.js";
import type { Actor } from "@/core/resources/actor/schema.js";
import { throwIfDeployFailed } from "@/core/resources/types.js";

function resolveActorsToDeploy(names: string[], allActors: Actor[]): Actor[] {
if (names.length === 0) return allActors;

const notFound = names.filter((n) => !allActors.some((a) => a.name === n));
if (notFound.length > 0) {
throw new InvalidInputError(
`Actor${notFound.length > 1 ? "s" : ""} not found in project: ${notFound.join(", ")}`,
);
}
return allActors.filter((a) => names.includes(a.name));
}

async function deployActorsAction(
{ log }: CLIContext,
names: string[],
): Promise<RunCommandResult> {
const { actors, project } = await readProjectConfig();
const toDeploy = resolveActorsToDeploy(names, actors);

if (toDeploy.length === 0) {
return {
outroMessage: `No actors found. Create actors in the '${project.actorsDir}' directory.`,
};
}

log.info(
`Found ${toDeploy.length} ${toDeploy.length === 1 ? "actor" : "actors"} to deploy`,
);

let completed = 0;
const total = toDeploy.length;

const results = await deployActorsSequentially(toDeploy, {
onStart: (startNames) => {
const label =
startNames.length === 1 ? startNames[0] : `${startNames.length} actors`;
log.step(
theme.styles.dim(`[${completed + 1}/${total}] Deploying ${label}...`),
);
},
onResult: (result) => {
completed++;
formatDeployResult(result, log);
},
});

const hasFailures = results.some((r) => r.status === "error");
if (hasFailures) {
log.message(buildDeploySummary(results, "actors"));
throwIfDeployFailed(results, "actor");
}

return { outroMessage: buildDeploySummary(results, "actors") };
}

export function getDeployCommand(): Command {
return new Base44Command("deploy")
.description("Deploy actors to Base44")
.argument("[names...]", "Actor names to deploy (deploys all if omitted)")
.action(async (ctx: CLIContext, rawNames: string[]) => {
const names = parseNames(rawNames);
return deployActorsAction(ctx, names);
});
}
10 changes: 10 additions & 0 deletions packages/cli/src/cli/commands/actors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Command } from "commander";
import { getDeleteCommand } from "./delete.js";
import { getDeployCommand } from "./deploy.js";

export function getActorsCommand(): Command {
return new Command("actors")
.description("Manage actors")
.addCommand(getDeployCommand())
.addCommand(getDeleteCommand());
}
10 changes: 1 addition & 9 deletions packages/cli/src/cli/commands/functions/delete.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command } from "@/cli/utils/index.js";
import { Base44Command, parseNames } from "@/cli/utils/index.js";
import { ApiError } from "@/core/errors.js";
import { deleteSingleFunction } from "@/core/resources/function/api.js";

Expand Down Expand Up @@ -42,14 +42,6 @@ async function deleteFunctionsAction(
return { outroMessage: parts.join(", ") };
}

/** Parse names from variadic CLI args, supporting comma-separated values. */
function parseNames(args: string[]): string[] {
return args
.flatMap((arg) => arg.split(","))
.map((n) => n.trim())
.filter(Boolean);
}

function validateNames(command: Command): void {
const names = parseNames(command.args);
if (names.length === 0) {
Expand Down
31 changes: 11 additions & 20 deletions packages/cli/src/cli/commands/functions/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import type { Logger } from "@base44-cli/logger";
import type { Command } from "commander";
import { formatDeployResult } from "@/cli/commands/functions/formatDeployResult.js";
import { parseNames } from "@/cli/commands/functions/parseNames.js";
import { CLIExitError } from "@/cli/errors.js";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, theme } from "@/cli/utils/index.js";
import {
Base44Command,
buildDeploySummary,
formatDeployResult,
parseNames,
theme,
} from "@/cli/utils/index.js";
import { InvalidInputError } from "@/core/errors.js";
import { readProjectConfig } from "@/core/index.js";
import {
deployFunctionsSequentially,
type PruneResult,
pruneRemovedFunctions,
type SingleFunctionDeployResult,
} from "@/core/resources/function/deploy.js";
import type { BackendFunction } from "@/core/resources/function/schema.js";
import { throwIfDeployFailed } from "@/core/resources/types.js";

function resolveFunctionsToDeploy(
names: string[],
Expand Down Expand Up @@ -45,18 +48,6 @@ function formatPruneSummary(pruneResults: PruneResult[], log: Logger): void {
}
}

function buildDeploySummary(results: SingleFunctionDeployResult[]): string {
const deployed = results.filter((r) => r.status === "deployed").length;
const unchanged = results.filter((r) => r.status === "unchanged").length;
const failed = results.filter((r) => r.status === "error").length;

const parts: string[] = [];
if (deployed > 0) parts.push(`${deployed} deployed`);
if (unchanged > 0) parts.push(`${unchanged} unchanged`);
if (failed > 0) parts.push(`${failed} error${failed !== 1 ? "s" : ""}`);
return parts.join(", ") || "No functions deployed";
}

async function deployFunctionsAction(
{ log }: CLIContext,
names: string[],
Expand Down Expand Up @@ -103,8 +94,8 @@ async function deployFunctionsAction(

const hasFailures = results.some((r) => r.status === "error");
if (hasFailures) {
log.message(buildDeploySummary(results));
throw new CLIExitError(1);
log.message(buildDeploySummary(results, "functions"));
throwIfDeployFailed(results, "function");
}

if (options.force) {
Expand Down Expand Up @@ -133,7 +124,7 @@ async function deployFunctionsAction(
formatPruneSummary(pruneResults, log);
}

return { outroMessage: buildDeploySummary(results) };
return { outroMessage: buildDeploySummary(results, "functions") };
}

export function getDeployCommand(): Command {
Expand Down
24 changes: 0 additions & 24 deletions packages/cli/src/cli/commands/functions/formatDeployResult.ts

This file was deleted.

Loading
Loading