Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d1a4055
feat(realtime): add realtime-handler resource and CLI commands
ImriKochWix Jun 30, 2026
ba5bf73
fix(lint): apply biome formatting and unused import fixes
ImriKochWix Jun 30, 2026
c076caa
fix(realtime): create handler inside base44/ dir, not project root
ImriKochWix Jun 30, 2026
97c3c85
fix(realtime): scaffold imports RealtimeHandler from @base44/sdk
ImriKochWix Jun 30, 2026
2db5dc4
fix(realtime): scaffold includes State/Message generic type parameters
ImriKochWix Jun 30, 2026
fe996cd
feat(types): auto-generate RealtimeHandlerRegistry from schema.jsonc
ImriKochWix Jun 30, 2026
da5ff95
fix(types): detect SDK package name and use module context in types.d.ts
ImriKochWix Jun 30, 2026
56df287
fix(lint): resolve Biome errors in realtime handler types
ImriKochWix Jun 30, 2026
0bd42fc
fix(realtime): use /realtime-handlers endpoint for handler deploy
ImriKochWix Jun 30, 2026
c99a4c7
fix(types): compile realtime messages as a named catalog, drop the regex
ImriKochWix Jul 5, 2026
c707723
feat(types)!: rename realtime schema sections inbound/outbound -> toC…
ImriKochWix Jul 5, 2026
0032dc3
refactor(cli): rename realtime -> actor (RealtimeHandler -> Actor)
ImriKochWix Jul 9, 2026
aab0a0e
fix(cli-ci): organize imports (biome) + pin npm@11 for publish
ImriKochWix Jul 9, 2026
5507903
feat(types): emit declare module for base44:runtime/actors
ImriKochWix Jul 26, 2026
afa3eae
refactor(types): base44:runtime/actors re-exports only Actor
ImriKochWix Jul 27, 2026
1b09f7e
feat(actor): scaffold imports Actor from base44:runtime/actors
ImriKochWix Jul 28, 2026
246a698
feat(actor): regenerate types after `actor new` so the scaffolded bas…
ImriKochWix Jul 28, 2026
bda3f21
fix(actor): scaffold matches the SDK Actor API
ImriKochWix Jul 30, 2026
e11b43c
fix(actor): make base44:runtime/actors actually resolve in the editor
ImriKochWix Jul 30, 2026
0c408c0
feat(actor): scaffold schema.jsonc and type the actor from ActorRegistry
ImriKochWix Jul 30, 2026
d40c110
fix(actor): scaffold a default export — the deploy bundler needs it
ImriKochWix Jul 30, 2026
01c2a05
ci: revert npm@11 pin in publish workflows (not needed)
ImriKochWix Jul 30, 2026
b0d7b88
remove scaffolding, will rely on a skill
talge-a11y Aug 10, 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`, included in unified `base44 deploy`; `base44 types generate` emits `ActorNameRegistry`.
- 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
24 changes: 18 additions & 6 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,17 @@ 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 its name is the path from the actors root (e.g. `actors/ChatRoom/entry.ts` → name `ChatRoom`; nesting is allowed). 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.

Deliberate gaps (vs functions): no `base44/shared/` inclusion, no `--force` prune, 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
```

## 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 @@ -136,10 +147,11 @@ 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)).
3. Actors (via `deployActorsSequentially()`)
4. Agent skills (via `agentSkillResource.push()`)
5. Agents (via `agentResource.push()`)
6. Connectors (via `pushConnectors()`) -- may return OAuth redirect URLs
7. 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)).

```bash
base44 deploy # With confirmation prompt
Expand Down
7 changes: 7 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ 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" } });
```

### Agent Mocks

```typescript
Expand Down
1 change: 1 addition & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ 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 |
| [`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
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 { CLIExitError } from "@/cli/errors.js";
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";

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 } = await readProjectConfig();
const toDeploy = resolveActorsToDeploy(names, actors);

if (toDeploy.length === 0) {
return {
outroMessage: "No actors found. Create actors in the 'actors' 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"));
throw new CLIExitError(1);
}

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);
});
}
8 changes: 8 additions & 0 deletions packages/cli/src/cli/commands/actors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Command } from "commander";
import { getDeployCommand } from "./deploy.js";

export function getActorsCommand(): Command {
return new Command("actors")
.description("Manage actors")
.addCommand(getDeployCommand());
}
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
27 changes: 9 additions & 18 deletions packages/cli/src/cli/commands/functions/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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";

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,7 +94,7 @@ async function deployFunctionsAction(

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

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.

34 changes: 30 additions & 4 deletions packages/cli/src/cli/commands/project/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {
filterPendingOAuth,
promptOAuthFlows,
} from "@/cli/commands/connectors/oauth-prompt.js";
import { formatDeployResult } from "@/cli/commands/functions/formatDeployResult.js";
import { maybeBuildBeforeDeploy } from "@/cli/commands/project/site-build.js";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import {
Base44Command,
formatDeployResult,
getConnectorsUrl,
getDashboardUrl,
theme,
Expand Down Expand Up @@ -48,8 +48,15 @@ export async function deployAction(
};
}

const { project, entities, functions, agents, connectors, authConfig } =
projectData;
const {
project,
entities,
functions,
actors,
agents,
connectors,
authConfig,
} = projectData;

// Build summary of what will be deployed
const summaryLines: string[] = [];
Expand All @@ -63,6 +70,11 @@ export async function deployAction(
` - ${functions.length} ${functions.length === 1 ? "function" : "functions"}`,
);
}
if (actors.length > 0) {
summaryLines.push(
` - ${actors.length} ${actors.length === 1 ? "actor" : "actors"}`,
);
}
if (agents.length > 0) {
summaryLines.push(
` - ${agents.length} ${agents.length === 1 ? "agent" : "agents"}`,
Expand Down Expand Up @@ -102,9 +114,11 @@ export async function deployAction(

await maybeBuildBeforeDeploy(ctx, project, options.build);

// Deploy resources with per-function progress
// Deploy resources with per-function and per-actor progress
let functionCompleted = 0;
const functionTotal = functions.length;
let actorCompleted = 0;
const actorTotal = actors.length;

const result = await deployAll(projectData, {
onVisibilitySet: (level) => {
Expand All @@ -122,6 +136,18 @@ export async function deployAction(
functionCompleted++;
formatDeployResult(r, log);
},
onActorStart: (names) => {
const label = names.length === 1 ? names[0] : `${names.length} actors`;
log.step(
theme.styles.dim(
`[${actorCompleted + 1}/${actorTotal}] Deploying ${label}...`,
),
);
},
onActorResult: (r) => {
actorCompleted++;
formatDeployResult(r, log);
},
});

// Handle connector-specific post-deploy flows
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/cli/commands/types/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const TYPES_FILE_PATH = "base44/.types/types.d.ts";
async function generateTypesAction({
runTask,
}: CLIContext): Promise<RunCommandResult> {
const { entities, functions, agents, connectors, project } =
const { entities, functions, agents, connectors, actors, project } =
await readProjectConfig();

await runTask("Generating types", async () => {
Expand All @@ -19,6 +19,7 @@ async function generateTypesAction({
functions,
agents,
connectors,
actors,
});
});

Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/cli/program.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Command, Option } from "commander";
import { getActorsCommand } from "@/cli/commands/actors/index.js";
import { getAgentSkillsCommand } from "@/cli/commands/agent-skills/index.js";
import { getAgentsCommand } from "@/cli/commands/agents/index.js";
import { getAuthCommand } from "@/cli/commands/auth/index.js";
Expand Down Expand Up @@ -95,6 +96,9 @@ export function createProgram(context: CLIContext): Command {
// Register functions commands
program.addCommand(getFunctionsCommand());

// Register actors commands
program.addCommand(getActorsCommand());

// Register workflows commands
program.addCommand(getWorkflowsCommand());

Expand Down
Loading