From a765c96e5e700dbc5c64f0cb7c3f613c137a5f18 Mon Sep 17 00:00:00 2001 From: talge-a11y Date: Mon, 17 Aug 2026 12:15:20 +0300 Subject: [PATCH 1/2] introduce actors skill --- skills/base44-cli/SKILL.md | 59 ++- skills/base44-cli/references/actors-create.md | 316 ++++++++++++++++ skills/base44-cli/references/actors-deploy.md | 153 ++++++++ skills/base44-cli/references/deploy.md | 21 +- .../base44-cli/references/types-generate.md | 16 +- skills/base44-sandbox/SKILL.md | 57 ++- skills/base44-sdk/SKILL.md | 41 ++- skills/base44-sdk/references/actors.md | 338 ++++++++++++++++++ 8 files changed, 979 insertions(+), 22 deletions(-) create mode 100644 skills/base44-cli/references/actors-create.md create mode 100644 skills/base44-cli/references/actors-deploy.md create mode 100644 skills/base44-sdk/references/actors.md diff --git a/skills/base44-cli/SKILL.md b/skills/base44-cli/SKILL.md index fed6809..feb556c 100644 --- a/skills/base44-cli/SKILL.md +++ b/skills/base44-cli/SKILL.md @@ -119,6 +119,9 @@ my-app/ │ ├── functions/ # Backend functions (optional) │ │ └── my-function/ │ │ └── entry.ts +│ ├── actors/ # Realtime actors (optional) +│ │ └── ChatRoom/ +│ │ └── entry.ts │ ├── agents/ # Agent configurations (optional) │ │ └── support_agent.jsonc │ ├── agent-skills/ # Agent skill instructions (optional) @@ -140,6 +143,7 @@ my-app/ - `base44/config.jsonc` - Project name, description, site build settings - `base44/entities/*.jsonc` - Data model schemas (see Entity Schema section) - `base44/functions/*/entry.ts` - Backend function entry point +- `base44/actors/*/entry.ts` - Realtime actor entry point (optional) - `base44/agents/*.jsonc` - Agent configurations (optional) - `base44/agent-skills/*.md` - Agent skill instructions (optional) - `base44/.types/types.d.ts` - Auto-generated TypeScript types for entities, functions, and agents (created by `npx base44 types generate`) @@ -154,6 +158,7 @@ my-app/ "visibility": "public", // Optional: "public" | "private" | "workspace" "entitiesDir": "./entities", // Optional: default "entities" "functionsDir": "./functions", // Optional: default "functions" + "actorsDir": "./actors", // Optional: default "actors" "agentsDir": "./agents", // Optional: default "agents" "agentSkillsDir": "./agent-skills", // Optional: default "agent-skills" "connectorsDir": "./connectors", // Optional: default "connectors" @@ -175,6 +180,7 @@ my-app/ | `visibility` | App visibility: `public`, `private`, or `workspace` | - | | `entitiesDir` | Directory for entity schemas | `"entities"` | | `functionsDir` | Directory for backend functions | `"functions"` | +| `actorsDir` | Directory for realtime actors | `"actors"` | | `agentsDir` | Directory for agent configs | `"agents"` | | `agentSkillsDir` | Directory for agent skill instructions | `"agent-skills"` | | `connectorsDir` | Directory for connector configs | `"connectors"` | @@ -274,7 +280,7 @@ Workspaces (a.k.a. organizations) group apps under shared membership. By default | Command | Description | Reference | |---------|-------------|-----------| -| `base44 deploy` | Deploy all resources (entities, functions, agents, agent skills, connectors, auth config, and site) | [deploy.md](references/deploy.md) | +| `base44 deploy` | Deploy all resources (entities, functions, actors, agents, agent skills, connectors, auth config, and site) | [deploy.md](references/deploy.md) | ### Entity Management @@ -322,6 +328,40 @@ For complete documentation, see [entities-create.md](references/entities-create. | `base44 functions list` | List all deployed functions on Base44 remote | [functions-list.md](references/functions-list.md) | | `base44 functions pull [name]` | Pull deployed functions from Base44 to local files | [functions-pull.md](references/functions-pull.md) | +### Actor Management + +Actors are stateful realtime server rooms over WebSockets — one live instance per room id, shared by every client connected to that id. Use them for multiplayer sessions, collaborative boards, presence and live cursors, in-room chat, and live auctions. + +| Action / Command | Description | Reference | +| ---------------- | ----------- | --------- | +| Create Actors | Define actors in `base44/actors` | [actors-create.md](references/actors-create.md) | +| `base44 actors deploy [names...]` | Deploy local actors to Base44; optionally target specific actors | [actors-deploy.md](references/actors-deploy.md) | + +#### Actor Layout (Quick Reference) + +**File naming:** `base44/actors/{ActorName}/entry.ts` — the folder is the actor's identity. + +```javascript +// base44/actors/ChatRoom/entry.ts +import { Actor } from "base44:runtime/actors"; + +export default class ChatRoom extends Actor { + handleConnect(conn) { conn.send({ type: "welcome" }); } + handleMessage(conn, msg) { this.broadcast({ type: "message", text: msg.text }); } + handleClose(conn) {} +} +``` + +**Naming rules:** actor names become a JavaScript class binding and the WebSocket connect handler — they must match `[A-Za-z_][A-Za-z0-9_]*` (max 128 chars, no `/`, `-`, `.` or `:`), must not be a JS reserved word, and cannot be nested in subfolders. +- Valid: `ChatRoom`, `BoardRoom`, `Lobby` +- Invalid: `chat-room`, `games/Arena`, `class` + +**Required:** `entry.ts` (or `entry.js`) that **default-exports** a class extending `Actor` from `base44:runtime/actors`. + +**Differs from functions:** only the actor's own folder is uploaded (no `base44/shared/`), no `--force` prune, no `list`/`pull`/`delete` commands, no local `base44 dev` runtime, and automations are not supported. + +For complete documentation, see [actors-create.md](references/actors-create.md). + ### Agent Management Agents are conversational AI assistants that can interact with users, access your app's entities, and call backend functions. Use these commands to manage agent configurations. @@ -484,9 +524,9 @@ Run one-off scripts against your app with the Base44 SDK pre-authenticated. Use | Command | Description | Reference | |---------|-------------|-----------| -| `base44 types generate` | Generate TypeScript types (`types.d.ts`) from entities, functions, agents, and connectors | [types-generate.md](references/types-generate.md) | +| `base44 types generate` | Generate TypeScript types (`types.d.ts`) from entities, functions, actors, agents, and connectors | [types-generate.md](references/types-generate.md) | -**Output:** `base44/.types/types.d.ts` — augments `@base44/sdk` module with typed registries (`EntityTypeRegistry`, `FunctionNameRegistry`, `AgentNameRegistry`, `ConnectorTypeRegistry`). +**Output:** `base44/.types/types.d.ts` — augments `@base44/sdk` module with typed registries (`EntityTypeRegistry`, `FunctionNameRegistry`, `AgentNameRegistry`, `ConnectorTypeRegistry`, `ActorNameRegistry`). **No authentication required.** Runs entirely locally. Automatically updates `tsconfig.json` to include the generated types. @@ -533,6 +573,7 @@ Or deploy individual resources: - `npx base44 functions delete ` - Delete a deployed function - `npx base44 functions list` - List all deployed functions - `npx base44 functions pull` - Pull deployed functions to local files +- `npx base44 actors deploy` - Deploy realtime actors only - `npx base44 agents push` - Push agents only - `npx base44 agent-skills push` - Push agent skills only - `npx base44 connectors pull` - Pull connectors from Base44 @@ -580,11 +621,11 @@ npx base44 deploy -y ### Generating TypeScript Types ```bash -# Generate types from entities, functions, agents, and connectors +# Generate types from entities, functions, actors, agents, and connectors npx base44 types generate ``` -This creates `base44/.types/types.d.ts` with typed registries for the `@base44/sdk` module. Run this after changing entities, functions, agents, or connectors to keep your types in sync. No authentication required. +This creates `base44/.types/types.d.ts` with typed registries for the `@base44/sdk` module. Run this after changing entities, functions, actors, agents, or connectors to keep your types in sync. No authentication required. ### Deploying Individual Resources ```bash @@ -598,6 +639,11 @@ npx base44 functions deploy my-function other-function # Deploy and prune removed functions npx base44 functions deploy --force +# Deploy only actors (all) +npx base44 actors deploy +# Deploy specific actors +npx base44 actors deploy ChatRoom BoardRoom + # Push only agents npx base44 agents push @@ -632,6 +678,9 @@ Most commands require authentication. If you're not logged in, the CLI will auto | No entities found | Ensure entities exist in `base44/entities/` directory | | Entity not recognized | Ensure file uses kebab-case naming (e.g., `team-member.jsonc` not `TeamMember.jsonc`) | | No functions found | Ensure functions exist in `base44/functions/` with `entry.ts` or `entry.js` | +| No actors found | Ensure actors exist as `base44/actors//entry.ts` (never directly in `base44/actors/`) | +| Invalid actor name | Actor names must match `[A-Za-z_][A-Za-z0-9_]*` (no `/`, `-`, `.` or `:`), avoid JS reserved words, and cannot be nested | +| Actor file rejected in the functions bucket | A file importing `base44:runtime/actors` must live at `base44/actors//entry.ts` — move it out of `base44/functions/` | | No agents found | Ensure agents exist in `base44/agents/` directory with valid `.jsonc` configs | | Invalid agent name | Agent names must be lowercase alphanumeric with underscores only | | No agent skills found | Ensure skill files exist in `base44/agent-skills/` directory with valid `.md` files | diff --git a/skills/base44-cli/references/actors-create.md b/skills/base44-cli/references/actors-create.md new file mode 100644 index 0000000..32f3c47 --- /dev/null +++ b/skills/base44-cli/references/actors-create.md @@ -0,0 +1,316 @@ +# Creating Actors + +Actors are Base44's realtime primitive: **stateful server rooms over WebSockets**. There is exactly one live instance per room id, and every client connected to that id shares it. The actor is authoritative — clients send inputs/operations, the actor validates them, applies them to its own state, and broadcasts the result. + +Actors are defined locally in your project and deployed to the Base44 backend, just like backend functions. + +## When to Use an Actor + +| Use an actor | Use something else | +|--------------|--------------------| +| Multiplayer sessions where users interact live | Single-user state → entities | +| Collaborative boards, docs, whiteboards | A page that just lists records live → `base44.entities.Thing.subscribe()` | +| Presence and live cursors | Async or request/response work → backend functions | +| In-room chat | Scheduled/background jobs → backend functions + automations | +| Live auctions, countdowns, shared timers | Anything that must attribute writes to a signed-in user → backend functions | + +## Actor Directory + +All actor definitions live in the `base44/actors/` folder. An actor is a folder containing an `entry.ts` file: + +``` +my-app/ + base44/ + actors/ + BoardRoom/ + entry.ts + ChatRoom/ + entry.ts +``` + +## How to Create an Actor + +1. Create a directory in `base44/actors/` named after the actor (PascalCase) +2. Create `entry.ts` in that directory and default-export a class extending `Actor` +3. Deploy it with `npx base44 actors deploy` + +## Actor Discovery and Naming + +The CLI discovers actors from `entry.ts` (or `entry.js`) files, and **the folder is the actor's identity** — the folder name becomes the actor name. + +| File | Actor name | +|------|------------| +| `base44/actors/BoardRoom/entry.ts` | `BoardRoom` | +| `base44/actors/ChatRoom/entry.ts` | `ChatRoom` | + +The name becomes the Durable Object class *and* the WebSocket connect handler, so it must be a plain JavaScript identifier: + +**Rules:** +- Must match `[A-Za-z_][A-Za-z0-9_]*`, max 128 characters — **no `-`, `.`, `/`, or `:`** +- Must not be a JavaScript reserved word (`class`, `default`, `new`, `static`, …) +- Must be a **single folder level** — `base44/actors/games/Arena/entry.ts` is not a valid actor (unlike functions, actors cannot be nested) +- Must not collide with a backend function name +- Use **PascalCase** by convention (it reads as a class, and it is one) + +| Valid | Invalid | Why | +|-------|---------|-----| +| `BoardRoom` | `board-room` | Hyphens are not valid in a JS identifier | +| `ChatRoom` | `chat.room` | Dots are not valid in a JS identifier | +| `Lobby` | `games/Arena` | Actors cannot be nested | +| `Room2` | `2Room` | Cannot start with a digit | +| `AuctionRoom` | `class` | Reserved word | + +All `*.js`, `*.ts`, `*.json`, and `*.jsonc` files under the actor folder are included when deploying. + +**Never name a helper `entry.ts`.** Every `entry.ts`/`entry.js` under `base44/actors/` is treated as an actor entry, at any depth — so `base44/actors/BoardRoom/lib/entry.ts` is discovered as an actor named `BoardRoom/lib` and rejected on deploy (names cannot contain `/`). Name helpers anything else. + +## Entry Point File + +The entry file **default-exports** a class extending `Actor`, imported from `base44:runtime/actors` — the only import that resolves the base class: + +```javascript +// base44/actors/BoardRoom/entry.ts +import { Actor } from "base44:runtime/actors"; + +const MAX_USERS = 32; + +export default class BoardRoom extends Actor { + users = new Map(); // conn.id -> { seat, cursor } + items = new Map(); // the shared, persisted room state + nextSeat = 1; + + async handleStart() { + // Runs on ANY wake (deploy, idle, hibernation). Rehydrate, then reconcile: + // a hibernation wake keeps sockets ATTACHED without re-running handleConnect. + this.items = new Map((await this.storage.get("items")) ?? []); + this.users = new Map((await this.storage.get("seats")) ?? []); + const live = new Set(this.getConnections().map((c) => c.id)); + for (const id of this.users.keys()) if (!live.has(id)) this.users.delete(id); + this.nextSeat = Math.max(0, ...[...this.users.values()].map((u) => u.seat)) + 1; + } + + async handleConnect(conn) { + if (!this.users.has(conn.id) && this.users.size >= MAX_USERS) { + conn.reject(4001, "room full"); // closes the socket but does NOT return + return; // from the handler — return immediately + } + // Reconnects are routine (network blips, reloads, redeploys): a returning + // id reclaims its entry — never demote it or mint a new seat. + if (!this.users.has(conn.id)) { + this.users.set(conn.id, { seat: this.nextSeat++, cursor: null }); + await this.saveSeats(); + } + conn.send({ type: "you", seat: this.users.get(conn.id).seat }); + conn.send({ type: "state", items: [...this.items.values()] }); // late joiners get full state + this.broadcastPresence(); + } + + async handleMessage(conn, msg) { + // Validate EVERYTHING at runtime: the payload is attacker-controlled and + // msg can even be null. Accept operations, never authoritative state. + if (typeof msg !== "object" || msg === null) return; + const user = this.users.get(conn.id); + if (!user) return; + + if (msg.type === "cursor") { + user.cursor = [Number(msg.x) || 0, Number(msg.y) || 0]; + this.broadcastPresence(); + } else if (msg.type === "upsert_item" && typeof msg.id === "string" && msg.id.length <= 64) { + const item = { id: msg.id, text: String(msg.text ?? "").slice(0, 2000) }; + this.items.set(item.id, item); + await this.storage.put("items", [...this.items.entries()]); // persist on change + this.broadcast({ type: "item", item }); + } + } + + async handleClose(conn) { + this.users.delete(conn.id); + await this.saveSeats(); + this.broadcastPresence(); + } + + saveSeats() { + return this.storage.put("seats", [...this.users.entries()]); + } + + broadcastPresence() { + // Project an explicit public shape — never spread whole server objects into + // a broadcast (they grow per-user secrets later). + this.broadcast({ + type: "presence", + users: [...this.users.values()].map((u) => ({ seat: u.seat, cursor: u.cursor })), + }); + } +} +``` + +The class name is cosmetic — the deploy re-exports your default export under the **folder** name. `export default class extends Actor { … }` works too. + +### Lifecycle Handlers + +| Handler | When it runs | +|---------|--------------| +| `handleConnect(conn)` | A client opened a connection to this room | +| `handleMessage(conn, msg)` | A client sent a message (parsed JSON) | +| `handleClose(conn)` | A connection closed | +| `handleStart()` | Optional. Any time the instance wakes (deploy, idle-out, hibernation) — before any connection is handled. Rehydrate state here | +| `handleWake(key)` | Optional. A timer armed with `this.schedule(key, at)` came due | + +Never override `onStart` or `onAlarm` — those are platform plumbing. + +### Instance API (`this.*`) + +| Member | Description | +|--------|-------------| +| `this.broadcast(data)` | Send a message to every connection in the room | +| `this.getConnections()` | Array of the live connections | +| `this.storage.get(key)` | Read persisted state (`Promise`) | +| `this.storage.put(key, value)` | Persist state | +| `this.storage.delete(key)` | Delete one key (`Promise`) | +| `this.storage.deleteAll()` | Wipe the room's storage — a later rejoin bootstraps like a brand-new room | +| `this.instanceId` | This room's instance id (the value the client connected with) | +| `this.schedule(key, at)` | Arm a one-shot wake at `at` (epoch ms or `Date`) | +| `this.cancelSchedule(key)` | Cancel a pending wake | +| `this.client` | An anonymous Base44 SDK client (see [Calling Base44](#calling-base44-from-an-actor)) | + +### The Connection Object (`conn`) + +| Member | Description | +|--------|-------------| +| `conn.id` | Per-connection identity, chosen by the client and reused across reconnects | +| `conn.send(data)` | Send a message to this one client | +| `conn.reject(code, reason)` | Refuse the connection (closes the socket; **`return` immediately after**) | + +`conn.id` is client-held. It's the right key for seats, roles, and reconnect reclamation — it is **never** trusted attribution. Durable per-user results (leaderboards, rewards, saved documents) must go through a signed-in path outside the actor. + +## State, Hibernation, and Reconnects + +Instance fields (`this.users`, `this.items`, …) live only as long as the room is awake. A quiet room hibernates after ~10 seconds **even with clients still connected**; `this.storage` is what survives. + +- Persist state you can't lose **when it changes**; never write high-frequency churn (every pointer move, every keystroke) to storage. +- Rehydrate in `handleStart()`, then **reconcile against `this.getConnections()`** — a hibernation wake keeps sockets attached and does **not** re-run `handleConnect`, so skipping this leaves every connected client unrecognized until it reconnects. +- Let a returning `conn.id` reclaim its entry (seat, role, score) instead of minting a new one. +- A reconnect that replaces a stale socket holding the same id closes the old one silently — `handleClose` does **not** fire for it, so the returning connection keeps its entry. Two **live** connections cannot share an id: the second is refused. That is why the client persists its connection id per tab (`sessionStorage`), never per browser. +- In sessions where a drop shouldn't instantly destroy state, give a missing id a short grace period; when the last client leaves mid-session, schedule the cleanup as a wake and cancel it if someone reconnects. + +`static options = { hibernate: false }` only makes a room non-hibernatable, not resident — it is still evicted after a couple of minutes idle, so storage remains the only durable answer. It is rarely needed. + +## Scheduled Wakes + +```javascript +await this.schedule("close_auction", Date.now() + 60_000); +// …later +await this.cancelSchedule("close_auction"); + +async handleWake(key) { + if (key === "close_auction") { + this.broadcast({ type: "auction_closed", winner: this.highBid }); + } +} +``` + +- Fires **even if the room is empty and asleep**. +- One-shot and coarse (±seconds); re-scheduling the same key overwrites it. +- Good for turn/forfeit timers, delayed cleanup of abandoned rooms, and absolute-time events. In-session countdowns should stay timestamp-driven on the client. + +## Broadcasting vs Per-Client Messages + +- `this.broadcast(data)` — **room-wide state** everyone should see. +- `conn.send(data)` — events about **one** client (your seat, your hand, your error). Broadcasting these leaks private state and makes every client react. + +Messages are JSON in both directions. `type` values beginning with `__` are reserved by the platform. + +## Durable Results + +When a session produces something that must outlive the room (the finished drawing, a chat transcript, an exported document): + +1. The **actor** broadcasts the authoritative result *and* writes it to `this.storage`, then re-`conn.send`s it to (re)connecting clients — a frontend cannot read actor storage, so that resend is the retry path. +2. The **frontend** persists it to entities. It has the signed-in user identity; the actor does not. + +Delivery is at-least-once, so the persistence step must be **idempotent**: key the record by the room's instance id and check for an existing record before creating one. Readers treat the earliest record per key as canonical. + +## Calling Base44 from an Actor + +Every actor has `this.client`, a ready-made `@base44/sdk` client acting as the app's **anonymous** role: + +```javascript +const rows = await this.client.entities.Room.filter({ status: "open" }); + +const res = await this.client.functions.invoke("settle_auction", { roomId: this.instanceId }); +const settled = res.data; // invoke() returns the raw response; the JSON is on .data +``` + +- Entity access is RLS-gated exactly like a logged-out visitor. +- It always operates on production data. +- It **cannot** act as a signed-in user — never route a user-attributed write through it. + +## Using Secrets + +Secrets work the same as in backend functions: + +```javascript +import { Actor } from "base44:runtime/actors"; +import { secrets } from "base44:runtime"; + +export default class PriceRoom extends Actor { + async handleStart() { + this.apiKey = secrets.get("MARKET_API_KEY"); + } +} +``` + +`BASE44_API_URL` and `BASE44_FUNCTIONS_VERSION` are reserved — the platform injects them for `this.client`, so a secret of either name is not readable from an actor. Pick another name. + +## Multi-File Actors + +An actor is not limited to `entry.ts`. Any `.js`, `.ts`, `.json`, or `.jsonc` file inside the actor's folder is uploaded on deploy and can be imported with a relative path: + +``` +base44/ + actors/ + BoardRoom/ + entry.ts ← import { sanitize } from "./sanitize.ts"; + sanitize.ts + limits.json +``` + +**The actor's own folder is the whole upload.** `base44 actors deploy` sends exactly the files under `base44/actors//` — unlike `functions deploy`, which also uploads the `base44/shared/` tree alongside every function. Keep the code an actor imports inside the actor's folder (copy it, or expose it through a backend function the actor calls with `this.client`). + +## Rooms and Discovery + +One actor instance = one session (one board, one match, one auction). Never funnel every user into a single global room. + +- **Cap capacity in `handleConnect`** and `conn.reject(...)` past the limit — the actor is the only place a cap can be enforced. +- **Browsable rooms:** keep a registry **entity** (e.g. `Room` with `status`, `user_count`) whose **record id is the actor instance id**. The registry is advertising; the actor is truth. List rooms by subscribing to the entity first, then fetching and reconciling by id, and filter to recently-updated rows (crashed rooms leave stale ones behind). +- **Private rooms:** there is no room-level auth. The instance id *is* the admission control, so mint it with `crypto.randomUUID()` (record ids are enumerable), keep it out of any readable registry, and share it only as an invite link or code. +- Instance ids are printable ASCII, 1–256 characters, and may not contain `/`. + +## Deploying Actors + +```bash +npx base44 actors deploy +``` + +Actors are also deployed as part of `npx base44 deploy`. For details, see [actors-deploy.md](actors-deploy.md). + +## Notes + +- Actors run on the Cloudflare backend; deploying one activates it if needed. +- Actors serve only the realtime WebSocket path — **automations are not supported** on an actor. Use a backend function if you need scheduled or entity-triggered work. +- `base44 dev` does not run actors locally; verify against a deployed actor. +- Use `npm:` specifiers for npm packages (e.g. `npm:zod`), same as in backend functions. +- Connecting from the frontend is `base44.actors.(instanceId).connect()` — see the base44-sdk skill's [actors.md](../../base44-sdk/references/actors.md). + +## Common Mistakes + +| Wrong | Correct | Why | +|-------|---------|-----| +| `base44/functions/ChatRoom/entry.ts` with `Actor` | `base44/actors/ChatRoom/entry.ts` | Actors have exactly one home; the actor import is rejected in the functions bucket | +| `import { Actor } from "@base44/sdk"` | `import { Actor } from "base44:runtime/actors"` | Only the virtual module resolves the base class at deploy time | +| `base44/actors/chat-room/entry.ts` | `base44/actors/ChatRoom/entry.ts` | The name becomes a JS class binding — no hyphens | +| `base44/actors/games/Arena/entry.ts` | `base44/actors/Arena/entry.ts` | Actors cannot be nested | +| `export class ChatRoom extends Actor` only | `export default class ChatRoom extends Actor` | The deploy re-exports the **default** export | +| `import { ok } from "../../shared/util.ts"` | Keep the helper inside the actor folder | Only the actor's own folder is uploaded | +| Storing state only in instance fields | `this.storage.put(...)` + rehydrate in `handleStart()` | Instance fields are lost when the room hibernates | +| `this.broadcast({ type: "your_hand", cards })` | `conn.send({ type: "your_hand", cards })` | Per-client events must not be broadcast | +| Trusting `msg.score` from a client | Recompute the outcome in the actor | Clients send inputs; the actor is authoritative | diff --git a/skills/base44-cli/references/actors-deploy.md b/skills/base44-cli/references/actors-deploy.md new file mode 100644 index 0000000..1a422b3 --- /dev/null +++ b/skills/base44-cli/references/actors-deploy.md @@ -0,0 +1,153 @@ +# base44 actors deploy + +Deploy local actor definitions to Base44. + +## Syntax + +```bash +npx base44 actors deploy [names...] +``` + +## Options + +| Option | Description | Required | +|--------|-------------|----------| +| `[names...]` | One or more actor names to deploy (deploys all if omitted). Space- or comma-separated | No | + +## Authentication + +**Required**: Yes. If not authenticated, you'll be prompted to login first. + +## What It Does + +1. Scans the `base44/actors/` directory (or `actorsDir` from `base44/config.jsonc`) for actor definitions +2. Discovers actors from `entry.ts`/`entry.js` files — the containing folder is the actor name +3. Displays the count of actors to be deployed +4. Uploads each actor's folder (all `*.js`, `*.ts`, `*.json`, `*.jsonc` files) to Base44, one actor at a time +5. Reports the results: deployed, unchanged, and failed counts + +## Prerequisites + +- Must be run from a Base44 project directory +- Project must have actor definitions in the `base44/actors/` folder +- Each actor is a folder with `entry.ts` (or `entry.js`) that default-exports a class extending `Actor` + +## Examples + +```bash +# Deploy all actors +npx base44 actors deploy + +# Deploy specific actors +npx base44 actors deploy ChatRoom BoardRoom + +# Comma-separated works too +npx base44 actors deploy ChatRoom,BoardRoom +``` + +## Output + +```bash +$ npx base44 actors deploy + +◆ Found 2 actors to deploy +◇ [1/2] Deploying ChatRoom... +✓ ChatRoom deployed (1.4s) +◇ [2/2] Deploying BoardRoom... +✓ BoardRoom unchanged + +└ 1 deployed, 1 unchanged +``` + +## Exit Codes + +- **Exit code 0**: All actors deployed successfully (or unchanged) +- **Exit code 1**: One or more actors failed to deploy + +A failing actor does not abort the run — the remaining actors are still attempted, then the command prints the full summary and exits with code 1. This makes it safe to use in CI pipelines where a partial failure should block the build. + +## Deploying as Part of the Project + +`npx base44 deploy` includes actors automatically, in this order: + +1. Entities +2. Functions +3. **Actors** +4. Agent skills +5. Agents +6. Auth config +7. Connectors +8. Site + +The confirmation summary lists the actor count alongside the other resources. + +## Configuration + +The actors directory is configurable in `base44/config.jsonc`: + +```jsonc +{ + "name": "My App", + "actorsDir": "./actors" // Optional: default "actors" +} +``` + +## Error Handling + +If no actors are found in your project: +```bash +$ npx base44 actors deploy +No actors found. Create actors in the 'actors' directory. +``` + +If a specified actor name doesn't exist locally: +```bash +$ npx base44 actors deploy Nonexistent +error: Actor not found in project: Nonexistent +``` + +If `entry.ts` sits directly in the actors directory: +```bash +$ npx base44 actors deploy +error: entry.ts found directly in the actors directory — it must be inside a named subfolder +``` + +## Troubleshooting + +| Error | Solution | +|-------|----------| +| `No actors found` | Ensure actors exist as `base44/actors//entry.ts` | +| `Actor not found in project: X` | Check the spelling; the actor name is the folder name, case-sensitive | +| `entry.ts found directly in the actors directory` | Move it into a named subfolder (`base44/actors/ChatRoom/entry.ts`) | +| `Duplicate actor name` | Two folders resolve to the same actor name — rename one | +| `Invalid actor name ''` | Actor names must match `[A-Za-z_][A-Za-z0-9_]*` (max 128 chars, no `/`, `-`, `.` or `:`) and not be a JavaScript reserved word. A name containing `/` means a nested folder — or a helper file named `entry.ts` — was picked up as an actor | +| `'X' exists as both a backend function and a base44/actors/X/ …` | An actor and a function cannot share a name — rename one | +| `'X' cannot have automations` | Actors serve only the realtime WebSocket path; move automation-triggered work into a backend function | +| Deploy rejects the actor as needing the Cloudflare backend | Actors require the Cloudflare runtime; deploying through this command activates it | + +## Differences from `functions deploy` + +| Capability | Functions | Actors | +|------------|-----------|--------| +| Deploy all / by name | Yes | Yes | +| `--force` prune of removed remotes | Yes | No | +| `list` / `pull` / `delete` subcommands | Yes | No | +| `base44/shared/` uploaded with the resource | Yes | No — only the actor's own folder | +| Local `base44 dev` runtime | Yes | No — verify against a deployed actor | +| Deployed in parallel | Yes | No — sequential, one actor at a time | + +## Use Cases + +- After creating a new actor in your project +- When modifying an actor's logic or message protocol +- To ship realtime changes before testing them in the app +- As part of your deploy workflow whenever realtime behavior changes + +## Notes + +- Deploy results per actor: `deployed`, `unchanged`, or `error` +- Changes are applied to your Base44 project immediately and served to connecting clients +- The whole actor folder ships on every deploy; anything the entry does not import is dropped from the bundle +- Actor definitions live in the `base44/actors/` directory, one folder per actor +- For how to create actors, see [actors-create.md](actors-create.md) +- For connecting to a deployed actor from your app, see the base44-sdk skill's [actors.md](../../base44-sdk/references/actors.md) diff --git a/skills/base44-cli/references/deploy.md b/skills/base44-cli/references/deploy.md index 1466c30..ff46072 100644 --- a/skills/base44-cli/references/deploy.md +++ b/skills/base44-cli/references/deploy.md @@ -1,6 +1,6 @@ # base44 deploy -Deploys all project resources (entities, functions, agents, agent skills, connectors, and site) to Base44 in a single command. +Deploys all project resources (entities, functions, actors, agents, agent skills, connectors, and site) to Base44 in a single command. ## Syntax @@ -20,12 +20,13 @@ The command automatically detects and deploys: 1. **Entities** - All `.jsonc` files in `base44/entities/` 2. **Functions** - All functions in `base44/functions/` -3. **Agents** - All agent configurations in `base44/agents/` -4. **Agent Skills** - All skill files in `base44/agent-skills/` -5. **Connectors** - All connector configurations in `base44/connectors/` -6. **Auth Config** - Authentication settings from `base44/auth/` (if present) -7. **Visibility** - App visibility (`public`, `private`, or `workspace`) from the `visibility` field in `base44/config.jsonc` (if set) -8. **Site** - Built files from `site.outputDirectory` (if configured) +3. **Actors** - All realtime actors in `base44/actors/` +4. **Agents** - All agent configurations in `base44/agents/` +5. **Agent Skills** - All skill files in `base44/agent-skills/` +6. **Connectors** - All connector configurations in `base44/connectors/` +7. **Auth Config** - Authentication settings from `base44/auth/` (if present) +8. **Visibility** - App visibility (`public`, `private`, or `workspace`) from the `visibility` field in `base44/config.jsonc` (if set) +9. **Site** - Built files from `site.outputDirectory` (if configured) ## Examples @@ -52,13 +53,14 @@ npx base44 deploy -y ## What It Does 1. Reads project configuration from `base44/config.jsonc` -2. Detects available resources (entities, functions, agents, agent skills, connectors, site) +2. Detects available resources (entities, functions, actors, agents, agent skills, connectors, site) 3. Shows a summary of what will be deployed 4. Asks for confirmation (unless `-y` flag is used) 5. Deploys all resources in sequence: - Sets app visibility (if configured) - Pushes entity schemas - Deploys functions + - Deploys actors - Pushes agent skill files - Pushes agent configurations - Pushes auth configuration @@ -90,7 +92,7 @@ After successful deployment: ## Notes - If no resources are found, the command exits with a message -- Use individual commands (`entities push`, `functions deploy`, `agents push`, `agent-skills push`, `connectors push`, `site deploy`) if you only want to deploy specific resources +- Use individual commands (`entities push`, `functions deploy`, `actors deploy`, `agents push`, `agent-skills push`, `connectors push`, `site deploy`) if you only want to deploy specific resources - The site must be built before deployment - this command does not run `npm run build` for you ## Related Commands @@ -99,6 +101,7 @@ After successful deployment: |---------|-------------| | `base44 entities push` | Push only entities | | `base44 functions deploy` | Deploy only functions | +| `base44 actors deploy` | Deploy only actors | | `base44 agents push` | Push only agents | | `base44 agent-skills push` | Push only agent skills | | `base44 connectors push` | Push only connectors | diff --git a/skills/base44-cli/references/types-generate.md b/skills/base44-cli/references/types-generate.md index ba4cd61..42ded4a 100644 --- a/skills/base44-cli/references/types-generate.md +++ b/skills/base44-cli/references/types-generate.md @@ -1,6 +1,6 @@ # `base44 types generate` -Generate TypeScript declaration file (`types.d.ts`) from project resources (entities, functions, agents, connectors). +Generate TypeScript declaration file (`types.d.ts`) from project resources (entities, functions, actors, agents, connectors). ## Usage @@ -10,7 +10,7 @@ npx base44 types generate ## What It Does -1. **Reads project configuration** — Scans `base44/entities/`, `base44/functions/`, `base44/agents/`, and `base44/connectors/` for all defined resources +1. **Reads project configuration** — Scans `base44/entities/`, `base44/functions/`, `base44/actors/`, `base44/agents/`, and `base44/connectors/` for all defined resources 2. **Generates `base44/.types/types.d.ts`** — Creates a TypeScript declaration file that augments the `@base44/sdk` module with typed registries 3. **Updates `tsconfig.json`** (if present) — Automatically adds `base44/.types/*.d.ts` to the `include` array so TypeScript picks up the generated types @@ -28,12 +28,17 @@ base44/.types/types.d.ts ### Generated Content -The declaration file augments the `@base44/sdk` module with four registries: +The declaration file augments the `@base44/sdk` module with five registries: - **`EntityTypeRegistry`** — Maps entity names to their TypeScript interfaces (compiled from entity JSON schemas) - **`FunctionNameRegistry`** — Lists all backend function names - **`AgentNameRegistry`** — Lists all agent names - **`ConnectorTypeRegistry`** — Lists all connector types +- **`ActorNameRegistry`** — Lists all actor names + +A registry with no entries is omitted from the file entirely. + +Actor **message** types are not generated — augment `ActorRegistry` by hand to type `subscribe` callbacks and `send` payloads (see the base44-sdk skill's [actors.md](../../base44-sdk/references/actors.md)). **Example output:** @@ -69,6 +74,10 @@ declare module '@base44/sdk' { interface ConnectorTypeRegistry { "googlecalendar": true; } + + interface ActorNameRegistry { + "ChatRoom": true; + } } ``` @@ -93,6 +102,7 @@ If the path is already included, or no `tsconfig.json` exists, this step is sile - After creating or modifying entity schemas in `base44/entities/` - After adding or removing backend functions in `base44/functions/` +- After adding or removing actors in `base44/actors/` - After adding or removing agents in `base44/agents/` - After adding or removing connectors in `base44/connectors/` - When setting up a TypeScript project for the first time with Base44 diff --git a/skills/base44-sandbox/SKILL.md b/skills/base44-sandbox/SKILL.md index b97c90a..d994a26 100644 --- a/skills/base44-sandbox/SKILL.md +++ b/skills/base44-sandbox/SKILL.md @@ -13,9 +13,9 @@ For **how to connect** to the sandbox (MCP endpoint or the `base44 sandbox` CLI, ## ⚡ The mental model: writing the file *is* the deploy -You are working on a **remote** app, not a local checkout. The project-level CLI workflow does **not** apply — never run `base44 deploy`, `base44 functions deploy`, `base44 ... push`, `base44 create`, or `base44 scaffold`. They assume a local project and a manual deploy step that does not exist here. +You are working on a **remote** app, not a local checkout. The project-level CLI workflow does **not** apply — never run `base44 deploy`, `base44 functions deploy`, `base44 actors deploy`, `base44 ... push`, `base44 create`, or `base44 scaffold`. They assume a local project and a manual deploy step that does not exist here. -Instead: **as soon as you write a resource file into the sandbox — a backend function, an entity, or an agent — the platform deploys/syncs it from there.** Your write is auto-committed (~5s debounce) and goes live. You do not run, and must not wait for, any `deploy` / `push` command. +Instead: **as soon as you write a resource file into the sandbox — a backend function, an actor, an entity, or an agent — the platform deploys/syncs it from there.** Your write is auto-committed (~5s debounce) and goes live. You do not run, and must not wait for, any `deploy` / `push` command. **One exception — connectors.** OAuth connectors aren't authored as files; they're set up against the remote app by its id, either with the MCP connector tools or with the dedicated, projectless `base44 connectors` commands (which take `--app-id` and need no local project). See [Connectors](#connectors-oauth-integrations) below. @@ -26,6 +26,7 @@ You *may* still use `run_command` (`sandbox run` in the CLI) for ordinary checks | Resource | Status in the sandbox | |----------|-----------------------| | **Backend functions** (`base44/functions/`) | ✅ Supported — write the files; they deploy from the sandbox. | +| **Actors** (`base44/actors/`) | ✅ Supported — write `entry.ts`; the actor deploys from the sandbox. Deleting the entry file tears it down. | | **Entities** (`base44/entities/`) | ✅ Supported — write the `.jsonc` schema file; it auto-syncs. No `entities push`. | | **Agents** (`base44/agents/`) | ✅ Supported — write the `.jsonc` config file; it auto-syncs. No `agents push`. | | **Frontend code** (`src/…`) | ✅ Supported — edit normally; HMR/preview reflects it. Use the **`base44-sdk`** skill for SDK API usage. | @@ -62,6 +63,56 @@ That's enough to author functions correctly. For deeper detail and more examples > **Calling the function from the frontend:** `base44.functions.invoke(name, data)` returns the **raw axios response** — your function's JSON is on **`.data`** (`const result = res.data`), not the top-level object, and it **throws on non-2xx** (error body at `err.response.data`). See the `base44-sdk` skill's [`functions.md`](../base44-sdk/references/functions.md) for details. +## Actors (realtime) + +Actors are stateful realtime server rooms over WebSockets — one live instance per room id, shared by everyone connected to that id. Reach for one when users interact **live in one shared session**: multiplayer, collaborative boards/docs, presence and live cursors, in-room chat, live auctions. A page that merely lists records live does not need an actor (`base44.entities.Thing.subscribe()` covers that). + +One folder per actor in `base44/actors/`, containing `entry.ts`. Just write the file — it deploys; **don't run `base44 actors deploy` or `deploy`.** Write **plain JavaScript**, exactly like a backend function — no type annotations; the file is `.ts` only because that is the entry contract. There is no test tool for an actor (it serves WebSockets, not requests) — verify it in the preview. + +``` +base44/actors/ + ChatRoom/ + entry.ts +``` + +```javascript +// base44/actors/ChatRoom/entry.ts +import { Actor } from "base44:runtime/actors"; // the only import that resolves the base class + +export default class ChatRoom extends Actor { + async handleStart() { + // Runs on every wake — instance fields are lost when the room hibernates. + this.history = (await this.storage.get("history")) ?? []; + } + handleConnect(conn) { + conn.send({ type: "history", messages: this.history }); // this client only + } + async handleMessage(conn, msg) { + if (msg?.type !== "message" || typeof msg.text !== "string") return; // validate everything + const entry = { from: conn.id, text: msg.text.slice(0, 2000) }; + this.history = [...this.history, entry].slice(-100); + await this.storage.put("history", this.history); + this.broadcast({ type: "message", ...entry }); // the whole room + } + handleClose(conn) {} +} +``` + +Conventions: +- **PascalCase** folder name — it becomes a JavaScript class binding, so `[A-Za-z_][A-Za-z0-9_]*` only (no `-`, `.`, `/`), no JS reserved words, and **no nested folders**. +- The entry must **default-export** a class extending `Actor`; the class name itself is cosmetic. +- Handlers: `handleConnect(conn)` / `handleMessage(conn, msg)` / `handleClose(conn)`, plus optional `handleStart()` and `handleWake(key)`. Never override `onStart`/`onAlarm`. +- Persist anything you can't lose in `this.storage` and rehydrate it in `handleStart()` — instance fields reset when the room hibernates. +- `this.broadcast(...)` for room-wide state; `conn.send(...)` for events about one client. +- `this.client` is an **anonymous** Base44 client (RLS-gated) for server-side reads and function calls. +- The actor is authoritative: clients send inputs, the actor validates and broadcasts. Never trust client-computed outcomes. +- Durable results (the finished drawing, a chat transcript): the actor broadcasts the result **and** writes it to `this.storage`, then re-`conn.send`s it to (re)connecting clients — the frontend cannot read actor storage, so that resend is the retry path. The **frontend** persists it to entities (it has the user identity). Delivery is at-least-once, so make that write idempotent: key the record by the room's instance id and check before creating. +- Automations are not supported on an actor. + +For the full authoring reference (naming, lifecycle, storage/hibernation, scheduled wakes, rooms and discovery), see the `base44-cli` skill's [`actors-create.md`](../base44-cli/references/actors-create.md) — but **ignore its "Deploying Actors" / CLI sections**, which assume a local project. + +> **Connecting from the frontend:** `base44.actors.ChatRoom(roomId).connect()` returns a connection with `.subscribe(cb)`, `.send(data)`, and `.close()`. Connect inside a `useEffect` and clean up both. See the `base44-sdk` skill's [`actors.md`](../base44-sdk/references/actors.md). + ## Entities One `.jsonc` file per entity in `base44/entities/`. Just write the file — it auto-syncs; **don't run `base44 entities push` or `deploy`.** @@ -194,7 +245,7 @@ https://app.base44.com/api/sandbox//local-agent/readme.md ## Workflow in the sandbox 1. **Orient** — `list_directory` / `read_file` / `grep` (`sandbox ls` / `sandbox read` / `sandbox grep` in the CLI) to understand the app before changing anything. -2. **Author** — create or edit resource files (backend functions, entities, agents) and frontend code following the conventions above; set up connectors via the connect flow. +2. **Author** — create or edit resource files (backend functions, actors, entities, agents) and frontend code following the conventions above; set up connectors via the connect flow. 3. **Verify** — optionally `run_command` (`sandbox run`) `npm run build` / `npx tsc --noEmit`, and use `get_app_preview_url` to eyeball changes (see `base44-remote-dev`). 4. **Let it ship** — do **nothing** to deploy. Writing the file is the deploy; the auto-commit (~5s) persists and ships it. Pause a moment after your last edit before disconnecting so the commit lands. 5. **(Optional) Checkpoint** — mark a known-good restore point the user can roll back to with `create_checkpoint` (`base44 sandbox checkpoint --name "..."` in the CLI). It flushes pending changes first, so the checkpoint captures your latest code. See `base44-remote-dev` for details. diff --git a/skills/base44-sdk/SKILL.md b/skills/base44-sdk/SKILL.md index f2ab89d..bf30fb8 100644 --- a/skills/base44-sdk/SKILL.md +++ b/skills/base44-sdk/SKILL.md @@ -116,6 +116,17 @@ Base44 SDK has unique method names. Do NOT assume patterns from Firebase, Supaba > **Exception:** an OpenAI-compatible client (e.g. the Vercel AI SDK) **is** correct when pointed at `base44.aiGateway.connection()` — that's how you build code agents (agent loops with tools). Use `InvokeLLM` only for a single call with no tools. See [ai-gateway.md](references/ai-gateway.md). +### Actors - WRONG vs CORRECT + +| ❌ WRONG (hallucinated) | ✅ CORRECT | +|------------------------|-----------| +| `actors.connect('ChatRoom', roomId)` | `actors.ChatRoom(roomId).connect()` | +| `actors.ChatRoom.connect(roomId)` | `actors.ChatRoom(roomId).connect()` | +| `actors.ChatRoom(roomId).subscribe(cb)` | subscribe on the **connection**: `const conn = actors.ChatRoom(roomId).connect(); conn.subscribe(cb)` | +| `room.on('message', cb)` | `room.subscribe(cb)` (one callback receives every message) | +| `room.emit(data)` | `room.send(data)` | +| `new WebSocket(...)` for app realtime | `base44.actors.(id).connect()` | + ### Entities - WRONG vs CORRECT | ❌ WRONG (hallucinated) | ✅ CORRECT | @@ -134,6 +145,7 @@ Base44 SDK has unique method names. Do NOT assume patterns from Firebase, Supaba | `auth` | Login, register, user management | [auth.md](references/auth.md) | | `agents` | AI conversations and messages | [base44-agents.md](references/base44-agents.md) | | `functions` | Backend function invocation | [functions.md](references/functions.md) | +| `actors` | Realtime rooms over WebSockets (multiplayer, collaboration, presence) | [actors.md](references/actors.md) | | `integrations` | AI, email, file uploads, custom APIs | [integrations.md](references/integrations.md) | | `aiGateway` | Connect an OpenAI-compatible SDK to Base44's AI gateway | [ai-gateway.md](references/ai-gateway.md) | | `analytics` | Track custom events and user activity | [analytics.md](references/analytics.md) | @@ -148,9 +160,9 @@ For client setup and authentication modes, see [client.md](references/client.md) Each reference file includes a "Type Definitions" section with TypeScript interfaces and types for the module's methods, parameters, and return values. -**Getting typed entities, functions, and agents:** The Base44 CLI generates types from your project resources (entities, functions, agents), including augmentations to `EntityTypeRegistry`, `FunctionNameRegistry`, and `AgentNameRegistry`, and wires them into your project so you get autocomplete and type checking without manual setup. For how to generate types, use the **base44-cli** skill. +**Getting typed entities, functions, actors, and agents:** The Base44 CLI generates types from your project resources (entities, functions, actors, agents), including augmentations to `EntityTypeRegistry`, `FunctionNameRegistry`, `ActorNameRegistry`, and `AgentNameRegistry`, and wires them into your project so you get autocomplete and type checking without manual setup. For how to generate types, use the **base44-cli** skill. -**Manual augmentation:** You can instead augment the registries yourself in a `.d.ts` file; see the Type Definitions sections in [entities.md](references/entities.md), [functions.md](references/functions.md), and [base44-agents.md](references/base44-agents.md). +**Manual augmentation:** You can instead augment the registries yourself in a `.d.ts` file; see the Type Definitions sections in [entities.md](references/entities.md), [functions.md](references/functions.md), [actors.md](references/actors.md), and [base44-agents.md](references/base44-agents.md). Actor **message** types are always hand-authored — augment `ActorRegistry` so the actor and its clients share one definition. ## Installation @@ -221,6 +233,10 @@ const base44 = createClient({ - Run server-side code → `functions.invoke()` - Need admin access → `base44.asServiceRole.functions.invoke()` +**Realtime shared session?** +- Multiplayer, collaborative board/doc, presence, live cursors, in-room chat → `actors.(roomId).connect()` (see [actors.md](references/actors.md)) +- Just keeping a list of records live on screen → `entities.EntityName.subscribe()`, **not** an actor + **External services?** - Send emails → `integrations.Core.SendEmail()` - Upload files → `integrations.Core.UploadFile()` @@ -279,6 +295,26 @@ export default async function (req) { } ``` +### Realtime Room (Actor) + +```javascript +// Frontend — connect inside useEffect and always clean up +useEffect(() => { + const room = base44.actors.ChatRoom(roomId).connect(); + const sub = room.subscribe((msg) => setMessages((prev) => [...prev, msg])); + return () => { sub.unsubscribe(); room.close(); }; +}, [roomId]); + +// Actor — base44/actors/ChatRoom/entry.ts +import { Actor } from "base44:runtime/actors"; + +export default class ChatRoom extends Actor { + handleConnect(conn) { conn.send({ type: "welcome" }); } // one client + handleMessage(conn, msg) { this.broadcast({ type: "message", text: msg.text }); } // everyone + handleClose(conn) {} +} +``` + ### Service Role Access Use `asServiceRole` in backend functions for admin-level operations: @@ -301,6 +337,7 @@ const token = await base44.asServiceRole.connectors.getAccessToken("slack"); | `agents` | Yes | Yes | | `functions.invoke()` | Yes | Yes | | `functions.fetch()` | Yes | Yes | +| `actors` (connect to a room) | Yes | No — the actor *is* the server side | | `integrations` | Yes | Yes | | `aiGateway` | No | Yes | | `analytics` | Yes | Yes | diff --git a/skills/base44-sdk/references/actors.md b/skills/base44-sdk/references/actors.md new file mode 100644 index 0000000..a8b92e4 --- /dev/null +++ b/skills/base44-sdk/references/actors.md @@ -0,0 +1,338 @@ +# Actors Module + +Connect to realtime server rooms via `base44.actors`. + +An **actor** is a stateful server room over WebSockets. There is one live instance per room id, every client connected to that id shares it, and the actor is authoritative — clients send inputs, the actor validates and broadcasts the result. + +## Contents +- [Methods](#methods) +- [Connecting](#connecting) (React, vanilla, reconnects) +- [Instance Ids](#instance-ids) +- [Authentication](#authentication) +- [Writing an Actor](#writing-an-actor) +- [When to Use an Actor](#when-to-use-an-actor) +- [Type Definitions](#type-definitions) + +## Methods + +### `base44.actors.(instanceId)` + +```javascript +base44.actors.ChatRoom(instanceId): ActorRef +``` + +- ``: the deployed actor's name — a property on `base44.actors`, **not** a string argument +- `instanceId`: the room id. Everyone who passes the same id shares one server instance +- Returns an `ActorRef` — a handle, not yet a connection + +### `connect` + +```javascript +actorRef.connect(options?): Connection +``` + +- `options.id` (optional): the connection id, which becomes the actor's `conn.id`. Supply a stable value so a reconnect reuses the same server-side identity; omit for an auto-generated one +- Returns a `Connection` synchronously — messages you send are buffered until the socket opens +- **Idempotent**: calling `connect()` again on the same ref returns the same connection + +### `Connection.subscribe` + +```javascript +connection.subscribe(callback): ActorSubscription +``` + +- `callback(data)`: called for every message the actor sends to this client +- Multiple listeners are allowed; returns `{ unsubscribe() }` which removes only that listener + +### `Connection.send` + +```javascript +connection.send(data): void +``` + +Sends a JSON message to the actor (arrives as `msg` in its `handleMessage`). + +### `Connection.close` + +```javascript +connection.close(): void +``` + +Tears down the socket, the heartbeat, and all listeners. + +### `Connection.id` + +The connection id the actor sees as `conn.id`. + +## Connecting + +### From React + +```javascript +import { useEffect, useRef, useState } from "react"; +import { base44 } from "@/api/base44Client"; + +function Board({ roomId }) { + const [items, setItems] = useState([]); + const roomRef = useRef(null); + + useEffect(() => { + // Persist the conn id per tab so a page RELOAD reclaims the same seat. + // A fresh id on every render would leak a seat per refresh. + let connId = sessionStorage.getItem("connId"); + if (!connId) { + connId = crypto.randomUUID(); + sessionStorage.setItem("connId", connId); + } + + const room = base44.actors.BoardRoom(roomId).connect({ id: connId }); + roomRef.current = room; + + const sub = room.subscribe((msg) => { + if (msg.type === "state") setItems(msg.items); + else if (msg.type === "item") setItems((prev) => [...prev, msg.item]); + // drop unknown types + }); + + return () => { + sub.unsubscribe(); + room.close(); + roomRef.current = null; + }; + }, [roomId]); + + const addItem = (text) => + roomRef.current?.send({ type: "upsert_item", id: crypto.randomUUID(), text }); + + return ; +} +``` + +Send **operations, not outcomes**, and throttle high-frequency input (cursor moves ~20–30/s max) — never send once per render or animation frame. + +**Always connect inside `useEffect` with a cleanup** — a bare `connect()` in the component body opens a socket per render. + +### Vanilla + +```javascript +const room = base44.actors.ChatRoom("lobby").connect(); +const sub = room.subscribe((msg) => console.log(msg)); + +room.send({ type: "message", text: "hi" }); + +// later +sub.unsubscribe(); +room.close(); +``` + +### Reconnects + +Reconnection, heartbeats, and half-open detection are handled by the SDK — you do not write retry logic. What you *do* control is identity: pass a stable `options.id` so the actor recognizes a returning client and can hand back its seat, role, or score. A reconnect replaces the stale socket holding that id without firing the actor's `handleClose`, which is what makes the seat reclaimable. + +Persist that id in **`sessionStorage`** (per tab), not `localStorage` — two *live* connections cannot share an id, so a second tab reusing it is refused. + +**React Native:** the same API applies, but there is no `sessionStorage` — keep the connection id in module scope or state. A fresh launch mints a new identity. + +`base44.cleanup()` closes every live room, so a forgotten `close()` cannot leak a heartbeat timer. + +## Instance Ids + +The instance id is what separates one room from another. + +- Printable ASCII, 1–256 characters, **no `/`** +- Use a meaningful id when the room is public or discoverable (e.g. a `Room` entity's record id) +- **Private rooms:** there is no room-level auth — the id *is* the admission control. Mint it with `crypto.randomUUID()`, keep it out of any readable list, and share it only as an invite link or code + +## Authentication + +The signed-in user's existing access token rides the connection automatically; nothing to mint or pass. Anonymous (logged-out) connections are allowed when the app permits them, and a login or logout is picked up on the next reconnect. + +`conn.id` on the server is chosen by the client, so it identifies a *connection*, not a person. It is the right key for seats and reconnects and the wrong key for anything that must be attributed to a user — do those writes through a backend function. + +## Writing an Actor + +Actors live in `base44/actors//entry.ts` and default-export a class extending `Actor`: + +```javascript +// base44/actors/ChatRoom/entry.ts +import { Actor } from "base44:runtime/actors"; + +export default class ChatRoom extends Actor { + async handleStart() { + this.history = (await this.storage.get("history")) ?? []; + } + + handleConnect(conn) { + conn.send({ type: "history", messages: this.history }); // just this client + this.broadcast({ type: "joined", id: conn.id }); // the whole room + } + + async handleMessage(conn, msg) { + if (msg?.type !== "message" || typeof msg.text !== "string") return; + const entry = { from: conn.id, text: msg.text.slice(0, 2000) }; + this.history = [...this.history, entry].slice(-100); + await this.storage.put("history", this.history); // survives hibernation + this.broadcast({ type: "message", ...entry }); + } + + handleClose(conn) { + this.broadcast({ type: "left", id: conn.id }); + } +} +``` + +Key rules: instance fields are lost when the room hibernates (persist in `this.storage`, rehydrate in `handleStart`), `this.broadcast()` is for room-wide state while `conn.send()` is for one client, and `this.client` is an **anonymous** Base44 client for server-side reads/calls. + +For the complete authoring contract — naming, lifecycle handlers, storage and hibernation, scheduled wakes, rooms and discovery, deployment — see [actors-create.md](../../base44-cli/references/actors-create.md) in base44-cli. + +## When to Use an Actor + +| Reach for `actors` | Reach for something else | +|--------------------|--------------------------| +| Multiplayer sessions, collaborative boards/docs | Live list of records → `base44.entities.Thing.subscribe()` | +| Presence, live cursors, typing indicators | Single-user state → `entities` | +| In-room chat, live auctions, shared timers | Request/response or background work → `functions.invoke()` | + +## Type Definitions + +**How to get typed actor names:** the Base44 CLI generates an augmentation of `ActorNameRegistry` from your project (`base44 types generate`). For how to run it, use the **base44-cli** skill. + +**Message types** are hand-authored in `ActorRegistry`, so the actor and the client share one source of truth: + +```typescript +declare module "@base44/sdk" { + interface ActorRegistry { + ChatRoom: { + toClient: + | { type: "history"; messages: { from: string; text: string }[] } + | { type: "message"; from: string; text: string } + | { type: "joined" | "left"; id: string }; + toServer: { type: "message"; text: string }; + }; + } +} +``` + +With that in place, `subscribe` callbacks and `send` payloads are typed: + +```typescript +const room = base44.actors.ChatRoom("lobby").connect(); +room.subscribe((msg) => { /* msg is the toClient union */ }); +room.send({ type: "message", text: "hi" }); // checked against toServer +``` + +Type the actor class off the same registry so the two cannot drift: + +```typescript +import { Actor } from "base44:runtime/actors"; +import type { ActorRegistry } from "@base44/sdk"; + +type Reg = ActorRegistry["ChatRoom"]; + +export default class ChatRoom extends Actor { /* … */ } +``` + +`base44:runtime/actors` is a virtual module resolved at deploy time, so add an ambient declaration for your editor (e.g. `base44/.types/runtime.d.ts`): + +```typescript +declare module "base44:runtime/actors" { + export { Actor, type Conn } from "@base44/sdk"; +} +``` + +### Interfaces + +```typescript +/** + * Registry of actor names. + * Auto-populated by `base44 types generate`. Do not edit by hand. + */ +interface ActorNameRegistry {} + +/** + * Registry of actor message types. + * Augment this interface to type subscribe callbacks and send payloads. + */ +interface ActorRegistry {} + +/** Options for ActorRef.connect(). */ +interface ActorConnectOptions { + /** The connection id — becomes the actor's `conn.id`. Omit for an auto-generated one. */ + id?: string; +} + +/** Handle for one listener registered via Connection.subscribe(). */ +interface ActorSubscription { + /** Remove this listener; other listeners and the socket stay live. */ + unsubscribe(): void; +} + +/** A live connection to an actor instance. */ +interface Connection { + /** The connection id (the value the actor sees as `conn.id`). */ + readonly id: string; + /** Register a message listener. Multiple are allowed. */ + subscribe(callback: (data: ToClientFor) => void): ActorSubscription; + /** Send a message. Buffered by the socket until it is open. */ + send(data: ToServerFor): void; + /** Tear down the socket, heartbeat, and all listeners. */ + close(): void; +} + +/** A handle to one actor instance — `base44.actors.MyActor(id)`. */ +interface ActorRef { + /** Open the WebSocket and return the Connection. Idempotent. */ + connect(options?: ActorConnectOptions): Connection; +} + +/** Client for a single named actor — call it with an instance id. */ +interface ActorClient { + (instanceId: string): ActorRef; +} +``` + +Server-side types (`Actor`, `Conn`, `Storage`) come from the actor base class: + +```typescript +interface Conn { + /** Unique per-connection id (one per socket/tab). */ + id: string; + send(data: Send): void; + reject(code: number, reason: string): void; +} + +interface Storage { + get(key: string): Promise; + put(key: string, value: unknown): Promise; + delete(key: string): Promise; + /** Wipe the room's entire persisted storage. */ + deleteAll(): Promise; +} + +abstract class Actor { + abstract handleConnect(conn: Conn): void | Promise; + abstract handleMessage(conn: Conn, msg: Incoming): void | Promise; + abstract handleClose(conn: Conn): void | Promise; + abstract handleTick(): void | Promise; + handleStart(): void | Promise; + protected handleWake(key: string): void | Promise; + protected schedule(key: string, at: number | Date): Promise; + protected cancelSchedule(key: string): Promise; + protected broadcast(data: Outgoing): void; + protected getConnections(): Conn[]; + protected get instanceId(): string; + protected get storage(): Storage; + /** Anonymous Base44 client scoped to this actor — RLS-gated, production data. */ + protected get client(): Base44Client; +} +``` + +`handleTick` is an abstract member, so a **TypeScript** actor has to declare it to compile — `handleTick() {}` is all it needs. Plain-JavaScript actors can omit it. + +## Notes + +- `base44.actors.(id)` returns a handle; you must call `.connect()` to get a `Connection` +- `subscribe`, `send`, and `close` live on the **`Connection`**, not on the ref +- Messages are JSON in both directions; `type` values beginning with `__` are reserved by the platform +- Actors are frontend-facing: the client connects from the browser, and the actor itself *is* the backend half +- Actors do not run under `base44 dev` — verify against a deployed actor From 37fab03cebfd93401723703b7b2fc1a09e77569f Mon Sep 17 00:00:00 2001 From: talge-a11y Date: Mon, 17 Aug 2026 14:23:36 +0300 Subject: [PATCH 2/2] adjustments --- README.md | 6 +- skills/base44-cli/SKILL.md | 24 +++++--- skills/base44-cli/references/actors-create.md | 61 +++++++++++++++++-- skills/base44-cli/references/actors-deploy.md | 35 +++++++++-- skills/base44-sandbox/SKILL.md | 6 +- skills/base44-sdk/SKILL.md | 19 ++++-- skills/base44-sdk/references/actors.md | 19 +++++- 7 files changed, 139 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 00c218a..679c133 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,11 @@ npx skills add base44/skills --skill base44-remote-dev --skill base44-sandbox -- | Skill | Description | |-------|-------------| -| [`base44-cli`](skills/base44-cli/SKILL.md) | Create and manage Base44 projects using the CLI. Handles resource configuration (entities, backend functions, AI agents), initialization, and deployment. | -| [`base44-sdk`](skills/base44-sdk/SKILL.md) | Build apps using the Base44 JavaScript SDK. Communicate with remote resources like entities, backend functions, and AI agents. | +| [`base44-cli`](skills/base44-cli/SKILL.md) | Create and manage Base44 projects using the CLI. Handles resource configuration (entities, backend functions, realtime actors, AI agents), initialization, and deployment. | +| [`base44-sdk`](skills/base44-sdk/SKILL.md) | Build apps using the Base44 JavaScript SDK. Communicate with remote resources like entities, backend functions, realtime actors, and AI agents. | | [`base44-troubleshooter`](skills/base44-troubleshooter/SKILL.md) | Troubleshoot production issues using backend function logs. Use when investigating app errors or diagnosing production problems. | | [`base44-remote-dev`](skills/base44-remote-dev/SKILL.md) | Develop a Base44 app remotely from your own coding agent by connecting it to the Base44 sandbox over MCP or the `base44 sandbox` CLI. | -| [`base44-sandbox`](skills/base44-sandbox/SKILL.md) | Author Base44 app code inside the cloud sandbox — no deploy/push; writing a resource file (function, entity, agent) into the sandbox is what ships it. | +| [`base44-sandbox`](skills/base44-sandbox/SKILL.md) | Author Base44 app code inside the cloud sandbox — no deploy/push; writing a resource file (function, actor, entity, agent) into the sandbox is what ships it. | ## About Agent Skills diff --git a/skills/base44-cli/SKILL.md b/skills/base44-cli/SKILL.md index feb556c..fd40238 100644 --- a/skills/base44-cli/SKILL.md +++ b/skills/base44-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: base44-cli -description: "The base44 CLI is used for EVERYTHING related to base44 projects: resource configuration (entities, backend functions, ai agents), initialization and actions (resource creation, deployment). This skill is the place for learning about how to configure resources. When you plan or implement a feature, you must learn this skill" +description: "The base44 CLI is used for EVERYTHING related to base44 projects: resource configuration (entities, backend functions, realtime actors, ai agents), initialization and actions (resource creation, deployment). This skill is the place for learning about how to configure resources, including actors — the realtime/WebSocket primitive behind multiplayer, collaborative boards, presence and live cursors, in-room chat, and live auctions. When you plan or implement a feature, you must learn this skill" metadata: sourcePackage: name: base44 @@ -336,6 +336,7 @@ Actors are stateful realtime server rooms over WebSockets — one live instance | ---------------- | ----------- | --------- | | Create Actors | Define actors in `base44/actors` | [actors-create.md](references/actors-create.md) | | `base44 actors deploy [names...]` | Deploy local actors to Base44; optionally target specific actors | [actors-deploy.md](references/actors-deploy.md) | +| `base44 actors delete ` | Tear down deployed actors (destroys the published script) | [actors-deploy.md](references/actors-deploy.md#deleting-a-deployed-actor) | #### Actor Layout (Quick Reference) @@ -346,19 +347,23 @@ Actors are stateful realtime server rooms over WebSockets — one live instance import { Actor } from "base44:runtime/actors"; export default class ChatRoom extends Actor { - handleConnect(conn) { conn.send({ type: "welcome" }); } - handleMessage(conn, msg) { this.broadcast({ type: "message", text: msg.text }); } + handleConnect(conn) { conn.send({ type: "welcome" }); } // this client only + handleMessage(conn, msg) { + // Always validate: the payload is attacker-controlled and msg can even be null. + if (msg?.type !== "message" || typeof msg.text !== "string") return; + this.broadcast({ type: "message", text: msg.text.slice(0, 2000) }); // the whole room + } handleClose(conn) {} } ``` -**Naming rules:** actor names become a JavaScript class binding and the WebSocket connect handler — they must match `[A-Za-z_][A-Za-z0-9_]*` (max 128 chars, no `/`, `-`, `.` or `:`), must not be a JS reserved word, and cannot be nested in subfolders. +**Naming rules:** actor names become a JavaScript class binding and the WebSocket connect handler — they must match `[A-Za-z_][A-Za-z0-9_]*` (max 128 chars, no `/`, `-`, `.` or `:`), must not be a JS reserved word, must not collide with a backend function name, and cannot be nested in subfolders. The CLI checks all of this locally before uploading; a folder with a dot in its name is skipped instead (so `ChatRoom.bak/` is safe scratch space). - Valid: `ChatRoom`, `BoardRoom`, `Lobby` - Invalid: `chat-room`, `games/Arena`, `class` -**Required:** `entry.ts` (or `entry.js`) that **default-exports** a class extending `Actor` from `base44:runtime/actors`. +**Required:** `entry.ts` (or `entry.js`) that **default-exports** a class extending `Actor` from `base44:runtime/actors`. In TypeScript, also declare `handleTick() {}` — it is an abstract member of `Actor`. -**Differs from functions:** only the actor's own folder is uploaded (no `base44/shared/`), no `--force` prune, no `list`/`pull`/`delete` commands, no local `base44 dev` runtime, and automations are not supported. +**Differs from functions:** only the actor's own folder is uploaded (no `base44/shared/`), no `--force` prune, no `list`/`pull` commands, no local `base44 dev` runtime, names cannot be path-like, and automations are not supported. For complete documentation, see [actors-create.md](references/actors-create.md). @@ -574,6 +579,7 @@ Or deploy individual resources: - `npx base44 functions list` - List all deployed functions - `npx base44 functions pull` - Pull deployed functions to local files - `npx base44 actors deploy` - Deploy realtime actors only +- `npx base44 actors delete ` - Tear down a deployed actor - `npx base44 agents push` - Push agents only - `npx base44 agent-skills push` - Push agent skills only - `npx base44 connectors pull` - Pull connectors from Base44 @@ -643,6 +649,8 @@ npx base44 functions deploy --force npx base44 actors deploy # Deploy specific actors npx base44 actors deploy ChatRoom BoardRoom +# Tear down a deployed actor +npx base44 actors delete ChatRoom # Push only agents npx base44 agents push @@ -679,7 +687,9 @@ Most commands require authentication. If you're not logged in, the CLI will auto | Entity not recognized | Ensure file uses kebab-case naming (e.g., `team-member.jsonc` not `TeamMember.jsonc`) | | No functions found | Ensure functions exist in `base44/functions/` with `entry.ts` or `entry.js` | | No actors found | Ensure actors exist as `base44/actors//entry.ts` (never directly in `base44/actors/`) | -| Invalid actor name | Actor names must match `[A-Za-z_][A-Za-z0-9_]*` (no `/`, `-`, `.` or `:`), avoid JS reserved words, and cannot be nested | +| Invalid actor name | Actor names must match `[A-Za-z_][A-Za-z0-9_]*` (no `/`, `-`, `.` or `:`), avoid JS reserved words, and cannot be nested. Caught locally, before any upload | +| `actors cannot be nested` | Flatten to `base44/actors//entry.ts`, or rename a helper you called `entry.ts` — every entry file under `base44/actors/` counts as an actor | +| Actor and function share a name | They deploy into one namespace — rename one of them | | Actor file rejected in the functions bucket | A file importing `base44:runtime/actors` must live at `base44/actors//entry.ts` — move it out of `base44/functions/` | | No agents found | Ensure agents exist in `base44/agents/` directory with valid `.jsonc` configs | | Invalid agent name | Agent names must be lowercase alphanumeric with underscores only | diff --git a/skills/base44-cli/references/actors-create.md b/skills/base44-cli/references/actors-create.md index 32f3c47..f0ac2a8 100644 --- a/skills/base44-cli/references/actors-create.md +++ b/skills/base44-cli/references/actors-create.md @@ -62,7 +62,7 @@ The name becomes the Durable Object class *and* the WebSocket connect handler, s All `*.js`, `*.ts`, `*.json`, and `*.jsonc` files under the actor folder are included when deploying. -**Never name a helper `entry.ts`.** Every `entry.ts`/`entry.js` under `base44/actors/` is treated as an actor entry, at any depth — so `base44/actors/BoardRoom/lib/entry.ts` is discovered as an actor named `BoardRoom/lib` and rejected on deploy (names cannot contain `/`). Name helpers anything else. +**Never name a helper `entry.ts`.** Every `entry.ts`/`entry.js` under `base44/actors/` is treated as an actor entry, at any depth — so `base44/actors/BoardRoom/lib/entry.ts` resolves to the nested name `BoardRoom/lib` and fails the "actors cannot be nested" check. The CLI's error names both causes (a genuinely nested actor, or a misnamed helper); rename the helper to anything else. ## Entry Point File @@ -84,15 +84,22 @@ export default class BoardRoom extends Actor { // a hibernation wake keeps sockets ATTACHED without re-running handleConnect. this.items = new Map((await this.storage.get("items")) ?? []); this.users = new Map((await this.storage.get("seats")) ?? []); + // Only a HIBERNATION wake still has sockets attached. A cold wake (deploy, + // idle-out) has none — and the client that triggered it is not attached yet — + // so pruning there would throw away every persisted seat. const live = new Set(this.getConnections().map((c) => c.id)); - for (const id of this.users.keys()) if (!live.has(id)) this.users.delete(id); + if (live.size > 0) { + for (const id of this.users.keys()) if (!live.has(id)) this.users.delete(id); + await this.saveSeats(); // persist the prune, or the next wake re-reads the stale map + } this.nextSeat = Math.max(0, ...[...this.users.values()].map((u) => u.seat)) + 1; } async handleConnect(conn) { + // reject() closes the socket but does not return from the handler — return yourself. if (!this.users.has(conn.id) && this.users.size >= MAX_USERS) { - conn.reject(4001, "room full"); // closes the socket but does NOT return - return; // from the handler — return immediately + conn.reject(4001, "room full"); + return; } // Reconnects are routine (network blips, reloads, redeploys): a returning // id reclaims its entry — never demote it or mint a new seat. @@ -155,6 +162,7 @@ The class name is cosmetic — the deploy re-exports your default export under t | `handleClose(conn)` | A connection closed | | `handleStart()` | Optional. Any time the instance wakes (deploy, idle-out, hibernation) — before any connection is handled. Rehydrate state here | | `handleWake(key)` | Optional. A timer armed with `this.schedule(key, at)` came due | +| `handleTick()` | The managed ticker's callback — see [The Managed Ticker (Opt-In)](#the-managed-ticker-opt-in). Declare it even if you never opt in: it is an abstract member, so a **TypeScript** actor needs `handleTick() {}` to compile (plain-JavaScript actors can omit it) | Never override `onStart` or `onAlarm` — those are platform plumbing. @@ -213,6 +221,43 @@ async handleWake(key) { - One-shot and coarse (±seconds); re-scheduling the same key overwrites it. - Good for turn/forfeit timers, delayed cleanup of abandoned rooms, and absolute-time events. In-session countdowns should stay timestamp-driven on the client. +## The Managed Ticker (Opt-In) + +`schedule()` handles *one* wake at an absolute time. When the room has to advance **on its own, repeatedly** — a game loop, a simulation step, a server-driven countdown — use the managed ticker instead. + +You opt in by overriding `shouldTick()`. While it returns `true`, the platform calls `handleTick()` every `tickIntervalMs` (default `100`). When it returns `false` the ticker stops and the room is free to idle out as usual. + +```javascript +import { Actor } from "base44:runtime/actors"; + +export default class Match extends Actor { + phase = "waiting"; + tickIntervalMs = 50; // 20 fps; default is 100 + + shouldTick() { + return this.phase === "playing"; // cheap and side-effect free — it runs every tick + } + + handleTick() { + this.advance(); // move the simulation forward + this.broadcast({ type: "frame", state: this.publicState() }); + if (this.isOver()) this.phase = "done"; // ticker stops on the next check + } + + handleMessage(conn, msg) { + if (msg?.type === "start") this.phase = "playing"; // ticking resumes from here + } +} +``` + +- **`shouldTick()` must be cheap and side-effect free.** It is consulted on every tick; do the work in `handleTick()`. +- **Don't write to `this.storage` every tick** — that is exactly the high-frequency churn to avoid. Persist at checkpoints (phase changes, round ends) and rehydrate in `handleStart()`; instance fields are still lost on a wake. +- **Don't re-arm the ticker from `handleTick()`** with `schedule()`. Flip the state that `shouldTick()` reads and let the platform manage the loop. +- A tick is not a delivery guarantee — clients can miss frames. Broadcast enough state to resynchronize, not just deltas. +- If the room only needs *one* future event, use [Scheduled Wakes](#scheduled-wakes); the ticker is for continuous advancement. + +`handleTick()` is an abstract member of `Actor`, so a **TypeScript** actor must declare it even when it never opts in — `handleTick() {}` is enough. Plain-JavaScript actors can omit it. + ## Broadcasting vs Per-Client Messages - `this.broadcast(data)` — **room-wide state** everyone should see. @@ -288,11 +333,14 @@ One actor instance = one session (one board, one match, one auction). Never funn ## Deploying Actors ```bash -npx base44 actors deploy +npx base44 actors deploy # all actors +npx base44 actors delete Lobby # tear one down on the server ``` Actors are also deployed as part of `npx base44 deploy`. For details, see [actors-deploy.md](actors-deploy.md). +Deleting the folder does **not** remove a deployed actor — the next deploy simply stops including it while the old one keeps serving. Run `actors delete` to tear it down. + ## Notes - Actors run on the Cloudflare backend; deploying one activates it if needed. @@ -309,7 +357,10 @@ Actors are also deployed as part of `npx base44 deploy`. For details, see [actor | `import { Actor } from "@base44/sdk"` | `import { Actor } from "base44:runtime/actors"` | Only the virtual module resolves the base class at deploy time | | `base44/actors/chat-room/entry.ts` | `base44/actors/ChatRoom/entry.ts` | The name becomes a JS class binding — no hyphens | | `base44/actors/games/Arena/entry.ts` | `base44/actors/Arena/entry.ts` | Actors cannot be nested | +| A helper at `base44/actors/BoardRoom/lib/entry.ts` | `base44/actors/BoardRoom/lib/helper.ts` | Every `entry.ts` under the actors dir is an actor entry, so this reads as the nested name `BoardRoom/lib` | | `export class ChatRoom extends Actor` only | `export default class ChatRoom extends Actor` | The deploy re-exports the **default** export | +| A TypeScript actor with no `handleTick` | Add `handleTick() {}` | It is an abstract member of `Actor`; the file will not compile without it | +| Deleting the folder to remove a deployed actor | `npx base44 actors delete ` | Removing the source only stops future deploys; the live actor keeps serving | | `import { ok } from "../../shared/util.ts"` | Keep the helper inside the actor folder | Only the actor's own folder is uploaded | | Storing state only in instance fields | `this.storage.put(...)` + rehydrate in `handleStart()` | Instance fields are lost when the room hibernates | | `this.broadcast({ type: "your_hand", cards })` | `conn.send({ type: "your_hand", cards })` | Per-client events must not be broadcast | diff --git a/skills/base44-cli/references/actors-deploy.md b/skills/base44-cli/references/actors-deploy.md index 1a422b3..b2022a7 100644 --- a/skills/base44-cli/references/actors-deploy.md +++ b/skills/base44-cli/references/actors-deploy.md @@ -81,6 +81,23 @@ A failing actor does not abort the run — the remaining actors are still attemp The confirmation summary lists the actor count alongside the other resources. +## Deleting a Deployed Actor + +```bash +npx base44 actors delete ChatRoom # one actor +npx base44 actors delete ChatRoom BoardRoom # several (comma-separated also works) +``` + +This tears the actor down on the server: it destroys the published script, so live clients lose the room and a later `actors deploy` starts it fresh. It is a **remote** operation — the local folder is untouched, and the actor does not need to still exist on disk for it to work. Delete the folder yourself if you don't want the next deploy to recreate the actor. + +At least one name is required. A name the server doesn't know is reported as `not found` rather than an error, so re-running the command is safe: + +```bash +$ npx base44 actors delete ChatRoom +✓ ChatRoom deleted +└ Actor "ChatRoom" deleted +``` + ## Configuration The actors directory is configurable in `base44/config.jsonc`: @@ -119,22 +136,28 @@ error: entry.ts found directly in the actors directory — it must be inside a n | `No actors found` | Ensure actors exist as `base44/actors//entry.ts` | | `Actor not found in project: X` | Check the spelling; the actor name is the folder name, case-sensitive | | `entry.ts found directly in the actors directory` | Move it into a named subfolder (`base44/actors/ChatRoom/entry.ts`) | -| `Duplicate actor name` | Two folders resolve to the same actor name — rename one | -| `Invalid actor name ''` | Actor names must match `[A-Za-z_][A-Za-z0-9_]*` (max 128 chars, no `/`, `-`, `.` or `:`) and not be a JavaScript reserved word. A name containing `/` means a nested folder — or a helper file named `entry.ts` — was picked up as an actor | -| `'X' exists as both a backend function and a base44/actors/X/ …` | An actor and a function cannot share a name — rename one | +| `Duplicate actor name` | One folder holds both `entry.js` and `entry.ts` — keep a single entry file | +| `Invalid actor name ''` | Actor names must match `[A-Za-z_][A-Za-z0-9_]*` (max 128 chars, no `/`, `-`, `.` or `:`) and not be a JavaScript reserved word. Rename the folder in PascalCase | +| `Invalid actor name '' — actors cannot be nested` | Flatten to one folder level, **or** rename a helper you called `entry.ts` (every entry file under `base44/actors/` counts as an actor) | +| `'X' exists as both a backend function and an actor` | Actors and functions share one deploy namespace — rename one of them | | `'X' cannot have automations` | Actors serve only the realtime WebSocket path; move automation-triggered work into a backend function | -| Deploy rejects the actor as needing the Cloudflare backend | Actors require the Cloudflare runtime; deploying through this command activates it | +| Deploy rejects the actor as needing the Cloudflare backend | Actors run only on the Cloudflare runtime. A **backend-less** app is activated automatically by this command, but an app already on the Deno runtime cannot host actors and is rejected — migrate the backend first | + +The name and nesting checks run **locally at discovery**, before anything uploads, so they cost no network round-trip and cannot leave a deploy half-applied. Everything else in this table comes back from the server. ## Differences from `functions deploy` | Capability | Functions | Actors | |------------|-----------|--------| | Deploy all / by name | Yes | Yes | +| `delete` subcommand | Yes | Yes — `base44 actors delete ` | | `--force` prune of removed remotes | Yes | No | -| `list` / `pull` / `delete` subcommands | Yes | No | +| `list` / `pull` subcommands | Yes | No | | `base44/shared/` uploaded with the resource | Yes | No — only the actor's own folder | | Local `base44 dev` runtime | Yes | No — verify against a deployed actor | -| Deployed in parallel | Yes | No — sequential, one actor at a time | +| Names may be nested / path-like | Yes (`foo/bar`) | No — one folder level, and a JS identifier | + +Both deploy **sequentially**, one item at a time. A failing item does not stop its siblings — the remaining actors (or functions) are still attempted — but it does abort the remaining **stages** of `base44 deploy`, so a bad actor blocks everything after step 3 (agent skills, agents, auth, connectors, site). ## Use Cases diff --git a/skills/base44-sandbox/SKILL.md b/skills/base44-sandbox/SKILL.md index d994a26..54f1647 100644 --- a/skills/base44-sandbox/SKILL.md +++ b/skills/base44-sandbox/SKILL.md @@ -1,6 +1,6 @@ --- name: base44-sandbox -description: "Develop a Base44 app remotely inside Base44's cloud sandbox using your own agent — no local checkout and no deploy/push commands. The implementation is remote: writing a resource file into the sandbox is what ships it (backend functions, entities, and agents all auto-sync from the file you write), and OAuth connectors are set up against the remote app via MCP tools or the projectless `base44 connectors` CLI. This skill is the place for learning what you can author in the sandbox, how backend functions, entities, and agents are structured, and how to connect a connector without a local filesystem. Triggers on 'develop my Base44 app remotely', 'no local files', 'cloud sandbox', 'create an entity/agent remotely', 'connect a connector remotely', 'bring my own agent', or any work editing a Base44 app inside a sandbox." +description: "Develop a Base44 app remotely inside Base44's cloud sandbox using your own agent — no local checkout and no deploy/push commands. The implementation is remote: writing a resource file into the sandbox is what ships it (backend functions, realtime actors, entities, and agents all auto-sync from the file you write), and OAuth connectors are set up against the remote app via MCP tools or the projectless `base44 connectors` CLI. This skill is the place for learning what you can author in the sandbox, how backend functions, actors, entities, and agents are structured, and how to connect a connector without a local filesystem. Triggers on 'develop my Base44 app remotely', 'no local files', 'cloud sandbox', 'create an entity/agent remotely', 'add realtime/multiplayer/presence remotely', 'connect a connector remotely', 'bring my own agent', or any work editing a Base44 app inside a sandbox." --- # Base44 in the Cloud Sandbox @@ -13,7 +13,7 @@ For **how to connect** to the sandbox (MCP endpoint or the `base44 sandbox` CLI, ## ⚡ The mental model: writing the file *is* the deploy -You are working on a **remote** app, not a local checkout. The project-level CLI workflow does **not** apply — never run `base44 deploy`, `base44 functions deploy`, `base44 actors deploy`, `base44 ... push`, `base44 create`, or `base44 scaffold`. They assume a local project and a manual deploy step that does not exist here. +You are working on a **remote** app, not a local checkout. The project-level CLI workflow does **not** apply — never run `base44 deploy`, `base44 functions deploy`, `base44 actors deploy`, `base44 actors delete`, `base44 ... push`, `base44 create`, or `base44 scaffold`. They assume a local project and a manual deploy step that does not exist here. Instead: **as soon as you write a resource file into the sandbox — a backend function, an actor, an entity, or an agent — the platform deploys/syncs it from there.** Your write is auto-committed (~5s debounce) and goes live. You do not run, and must not wait for, any `deploy` / `push` command. @@ -101,7 +101,7 @@ export default class ChatRoom extends Actor { Conventions: - **PascalCase** folder name — it becomes a JavaScript class binding, so `[A-Za-z_][A-Za-z0-9_]*` only (no `-`, `.`, `/`), no JS reserved words, and **no nested folders**. - The entry must **default-export** a class extending `Actor`; the class name itself is cosmetic. -- Handlers: `handleConnect(conn)` / `handleMessage(conn, msg)` / `handleClose(conn)`, plus optional `handleStart()` and `handleWake(key)`. Never override `onStart`/`onAlarm`. +- Handlers: `handleConnect(conn)` / `handleMessage(conn, msg)` / `handleClose(conn)`, plus optional `handleStart()` and `handleWake(key)`. For a room that must advance on its own (game loop, visible countdown), override `shouldTick()` and the platform calls `handleTick()` every `tickIntervalMs` (default 100) while it returns true. Never override `onStart`/`onAlarm`. - Persist anything you can't lose in `this.storage` and rehydrate it in `handleStart()` — instance fields reset when the room hibernates. - `this.broadcast(...)` for room-wide state; `conn.send(...)` for events about one client. - `this.client` is an **anonymous** Base44 client (RLS-gated) for server-side reads and function calls. diff --git a/skills/base44-sdk/SKILL.md b/skills/base44-sdk/SKILL.md index bf30fb8..dd29b56 100644 --- a/skills/base44-sdk/SKILL.md +++ b/skills/base44-sdk/SKILL.md @@ -1,6 +1,6 @@ --- name: base44-sdk -description: "The base44 SDK is the library to communicate with base44 services. In projects, you use it to communicate with remote resources (entities, backend functions, ai agents) and to write backend functions. This skill is the place for learning about available modules and types. When you plan or implement a feature, you must learn this skill" +description: "The base44 SDK is the library to communicate with base44 services. In projects, you use it to communicate with remote resources (entities, backend functions, realtime actors, ai agents) and to write backend functions and actors. This skill is the place for learning about available modules and types, including actors — the realtime/WebSocket primitive for multiplayer, collaborative boards, presence and live cursors, in-room chat, and live auctions. When you plan or implement a feature, you must learn this skill" --- # Base44 Coder @@ -126,6 +126,9 @@ Base44 SDK has unique method names. Do NOT assume patterns from Firebase, Supaba | `room.on('message', cb)` | `room.subscribe(cb)` (one callback receives every message) | | `room.emit(data)` | `room.send(data)` | | `new WebSocket(...)` for app realtime | `base44.actors.(id).connect()` | +| `const unsub = room.subscribe(cb); unsub()` | `const sub = room.subscribe(cb); sub.unsubscribe()` | + +> **The two `subscribe()` methods return different shapes.** `Connection.subscribe()` (actors) returns an object — `sub.unsubscribe()`. `entities..subscribe()` returns the unsubscribe **function** itself — `unsub()`. Don't carry one convention to the other. ### Entities - WRONG vs CORRECT @@ -301,16 +304,22 @@ export default async function (req) { // Frontend — connect inside useEffect and always clean up useEffect(() => { const room = base44.actors.ChatRoom(roomId).connect(); - const sub = room.subscribe((msg) => setMessages((prev) => [...prev, msg])); - return () => { sub.unsubscribe(); room.close(); }; -}, [roomId]); + const sub = room.subscribe((msg) => { + if (msg.type === "message") setMessages((prev) => [...prev, msg]); // switch on type; drop unknowns + }); + return () => { sub.unsubscribe(); room.close(); }; // NOTE: actors return { unsubscribe() }, +}, [roomId]); // entities.subscribe() returns the function itself // Actor — base44/actors/ChatRoom/entry.ts import { Actor } from "base44:runtime/actors"; export default class ChatRoom extends Actor { handleConnect(conn) { conn.send({ type: "welcome" }); } // one client - handleMessage(conn, msg) { this.broadcast({ type: "message", text: msg.text }); } // everyone + handleMessage(conn, msg) { + // Always validate: the payload is attacker-controlled and msg can even be null. + if (msg?.type !== "message" || typeof msg.text !== "string") return; + this.broadcast({ type: "message", text: msg.text.slice(0, 2000) }); // everyone + } handleClose(conn) {} } ``` diff --git a/skills/base44-sdk/references/actors.md b/skills/base44-sdk/references/actors.md index a8b92e4..d13d843 100644 --- a/skills/base44-sdk/references/actors.md +++ b/skills/base44-sdk/references/actors.md @@ -33,7 +33,7 @@ actorRef.connect(options?): Connection - `options.id` (optional): the connection id, which becomes the actor's `conn.id`. Supply a stable value so a reconnect reuses the same server-side identity; omit for an auto-generated one - Returns a `Connection` synchronously — messages you send are buffered until the socket opens -- **Idempotent**: calling `connect()` again on the same ref returns the same connection +- **Idempotent per ref**: calling `connect()` again on the *same* ref returns the same connection. But `base44.actors.ChatRoom(id)` mints a **new** ref on every call, so `base44.actors.ChatRoom(id).connect()` opens a **new** socket each time — hold the ref (or the connection) yourself, as the React example does ### `Connection.subscribe` @@ -267,6 +267,12 @@ interface ActorSubscription { unsubscribe(): void; } +/** Resolve a name to its message types via ActorRegistry; `unknown` if untyped. */ +type ToClientFor = + N extends keyof ActorRegistry ? ActorRegistry[N]["toClient"] : unknown; +type ToServerFor = + N extends keyof ActorRegistry ? ActorRegistry[N]["toServer"] : unknown; + /** A live connection to an actor instance. */ interface Connection { /** The connection id (the value the actor sees as `conn.id`). */ @@ -313,9 +319,10 @@ abstract class Actor { abstract handleConnect(conn: Conn): void | Promise; abstract handleMessage(conn: Conn, msg: Incoming): void | Promise; abstract handleClose(conn: Conn): void | Promise; + /** The managed ticker's callback. Runs only while `shouldTick()` returns true. */ abstract handleTick(): void | Promise; handleStart(): void | Promise; - protected handleWake(key: string): void | Promise; + handleWake(key: string): void | Promise; protected schedule(key: string, at: number | Date): Promise; protected cancelSchedule(key: string): Promise; protected broadcast(data: Outgoing): void; @@ -324,11 +331,19 @@ abstract class Actor { protected get storage(): Storage; /** Anonymous Base44 client scoped to this actor — RLS-gated, production data. */ protected get client(): Base44Client; + /** Override to opt into the managed ticker; must be cheap and side-effect free. */ + shouldTick?(): boolean; + /** Ticker period in ms. Default 100. */ + tickIntervalMs: number; } ``` +The members you **implement** (`handleConnect`, `handleMessage`, `handleClose`, `handleTick`, `handleStart`, `handleWake`, `shouldTick`, `tickIntervalMs`) are public — override them without a modifier. The members you only **call** (`broadcast`, `getConnections`, `schedule`, `cancelSchedule`, `instanceId`, `storage`, `client`) are `protected`: reachable via `this` inside your actor, invisible from outside it. Marking an override `protected` where the base declares it public is a compile error (`TS2416`), so don't add the modifier to a handler. + `handleTick` is an abstract member, so a **TypeScript** actor has to declare it to compile — `handleTick() {}` is all it needs. Plain-JavaScript actors can omit it. +It only *runs* if you opt into the managed ticker by overriding `shouldTick()`: the platform then calls `handleTick()` every `tickIntervalMs` while that returns true, and stops (letting the room idle out) when it returns false. Reach for it when the room must advance on its own — a game loop, a visible countdown, a simulation step — and see the base44-cli skill's [actors-create.md](../../base44-cli/references/actors-create.md#the-managed-ticker-opt-in) for the full pattern. + ## Notes - `base44.actors.(id)` returns a handle; you must call `.connect()` to get a `Connection`