Keywords: resource, entity, function, actor, agent, agent skill, connector, push, readAll, deploy, site, tar.gz, deployAll, ProjectData, explicit-delete, mirrored, prune, --force
Resources are project-specific collections (entities, functions, actors, agents, agent skills, connectors) that can be read from the filesystem and pushed to the Base44 API.
Defined in packages/cli/src/resources/types.ts:
export interface Resource<T> {
readAll: (dir: string) => Promise<T[]>;
push: (items: T[]) => Promise<unknown>;
}The push method handles empty arrays gracefully (returns early without making an API call).
Every resource falls into one of two groups, by what happens to a remote item that is missing locally:
| Group | Resources | deploy |
Removal |
|---|---|---|---|
| Explicit-delete | functions, actors, connectors | Adds and updates; never removes | functions deploy --force, actors delete, connectors push --force |
| Mirrored | entities, agents, agent skills | Makes the remote match the folder | Delete the local file, then deploy or <resource> push |
In both groups an empty local list removes nothing, even with --force. Put a new resource in the explicit-delete group when its remote copy holds something the folder can't restore (credentials, a Stripe sandbox, durable storage) or when it can be created outside the folder.
Each resource follows a consistent file structure inside packages/cli/src/core/resources/<name>/:
<name>/
├── schema.ts # Zod schemas for validation
├── config.ts # File reading logic (reads from filesystem)
├── resource.ts # Resource<T> implementation
├── api.ts # API calls (push to server)
└── index.ts # Barrel exports
Example implementation:
// resources/<name>/resource.ts
export const entityResource: Resource<Entity> = {
readAll: readAllEntities,
push: pushEntities,
};A resource schema validates files the server wrote and will read back, so the
server's validation is the contract — never be stricter than it. Anything stricter
rejects an app the server accepts, and because readProjectConfig() reads every
resource up front, one such file fails every command (deploy,
entities push/pull, functions deploy) whether or not it touches that resource.
A hand-derived entity schema once rejected a quarter of real publishes this way.
So before adding or narrowing a rule, confirm the server rejects that shape too.
Rejecting something the server cannot evaluate — an unsupported operator, or an
operator where only exact equality is applied — is correct; that's a real defect,
not strictness. base44 dev interprets these same files locally, so a schema
change usually needs a matching change in src/cli/dev/dev-server/db/.
- Create folder:
packages/cli/src/core/resources/<name>/ - Add
schema.tswith Zod schemas - Add
config.tswith file reading logic - Add
resource.tsimplementingResource<T> - Add
api.tsfor API calls - Add
index.tsbarrel exports - Update
packages/cli/src/core/resources/index.tsto export the new resource - Register in
packages/cli/src/core/project/config.ts(add toreadProjectConfig) - Add typed field to
ProjectDatainterface
Functions are read from the project's functions directory (e.g. base44/functions/ or path from config.jsonc). Two discovery modes:
Config-based: A folder that contains function.jsonc (or function.json) is a function. The config defines name, entry (path to the handler file), and optional automations. The config file can live at any depth under the functions dir (e.g. functions/foo/bar/function.jsonc). All *.js, *.ts, and *.json files in that folder and subfolders are included when deploying.
Zero-config: A folder that contains entry.js or entry.ts and has no function.jsonc in the same folder is also a function. The function name is the path from the functions root to that folder (e.g. functions/foo/bar/hello/entry.ts → name foo/bar/hello). File collection is recursive: all **/*.{js,ts,json} under that folder are included.
If both exist in the same folder (e.g. function.jsonc and entry.ts), the config wins: the function is loaded from the config and the name/entry come from the config file. Duplicate function names (same path or same config name) cause an error.
Deploy ships file contents verbatim — the source is never parsed or linted — so this contract is enforced only when running locally. An entry file default-exports an async request handler:
export default async function (req) { ... }— takes aRequest, returns aResponse.
Entry files may also import secrets and waitUntil from base44:runtime. Locally, base44 dev runs functions on workerd via Miniflare by default — each function is bundled with esbuild + @deno/loader (src/cli/dev/dev-server/function-bundler.ts), with base44:runtime served as a virtual module, secrets as real Worker env bindings and waitUntil riding ctx.waitUntil. A fallback runtime covers installations where workerd is unavailable (compiled binaries, B44_DEV_FUNCTIONS_RUNTIME=deno) and supplies base44:runtime via an import map. A project-level deno.json import map is not applied to functions — locally or deployed — since only files under base44/ are uploaded. See packages/cli/backend-runtime/README.md for the local implementation and its intentional differences from production.
The resource implementation is in src/core/resources/actor/. The upload adapter
reads discovered files with the existing file utilities and strips local paths
and source metadata. Actor payloads never use function shared-file assembly.
The platform validates the source and handles publication.
ProjectConfigReader loads project actors, skips actor discovery for imported
plugins, and checks actor names against the final merged function names.
src/core/types/generator.ts augments the SDK with a top-level import so registry-only
output preserves SDK exports. Message declarations remain user-owned.
The dev server registers its actor handler before the production proxy. Keep that registration unconditional; it also covers requests for actors absent from the local checkout.
Agent skills are app-scoped instruction snippets shared across the app's agents. Unlike other resources they are stored as one markdown file per skill under the agent-skills directory (base44/agent-skills/, or agentSkillsDir in config.jsonc): the filename (without .md) is the skill name, the frontmatter description is the summary, and the body is the instruction text. Agents reference skills by name via selected_skill_names; selected_workspace_skill_ids (org-shared workspace skills) is not managed here and is passed through pull/push/deploy untouched.
The workflow module at packages/cli/src/core/resources/workflow/ is read-only — workflows are authored in the builder, not pulled/pushed from local files, so there is no Resource<T> implementation. It exposes listWorkflows() and listWorkflowRuns(filters) over GET /api/apps/{app_id}/workflows[/runs], consumed by the workflows list / workflows runs commands. Apps that predate the Workflows system return 403 from these endpoints; the commands translate that into an explanation.
The site module at packages/cli/src/core/site/ handles deploying an app's built output. It follows a different pattern than resources — there is no item list, so no readAll/push.
It owns which transport ships the build, but not the shipping itself: deploymentsApiEnabled() in deployment.ts only decides, and base44 site deploy calls the chosen flow.
import { deploymentsApiEnabled } from "@/core/site/index.js";
const viaDeployments = deploymentsApiEnabled();- Gate on → the deployments API, see deployments.md. Whether the build carries a worker changes what that flow sends, never which flow runs, and a worker brings its own assets directory — so the command may pass a null
outputDir. - Gate off → the legacy tar.gz path: tar.gz
site.outputDirectoryand upload viaPOST /api/apps/{app_id}/deploy-dist. It tarssite.outputDirectory, or./distwhen the config names none.
Each flow validates its own inputs, so the decision itself is a boolean and needs nothing from the tree.
base44 deploy does not go through this. It ships the site through deployAll()'s legacy tar.gz step, so the deployments-API transport is reachable only from base44 site deploy — it needs a commit address the unified deploy has no way to take.
One flow per transport: deployment.ts (deployments API, worker or not) and deploy.ts (legacy tar.gz). The first uses manifest.ts, modules.ts, upload.ts, wrangler-config.ts, git-hash.ts, and the module's api.ts / schema.ts; see deployments.md.
- Validate output directory exists and has files
- Create temporary tar.gz archive using
tarpackage - Upload archive to the API
- Parse response with Zod schema
- Clean up temporary archive file
How a broken base44/config.jsonc is read depends on whether the command touches resources (core/project/config.ts, core/project/fallback.ts):
| Reader | Used by | Missing / unreadable config | Invalid name, description, site |
Invalid visibility, *Dir, plugin, plugins |
|---|---|---|---|---|
readProjectSettingsOrDefaults |
site install, site dev |
template defaults (a missing one only in a folder with a package.json) |
defaulted | defaulted |
readProjectSettings / readProjectConfig |
everything else | fails | defaulted | fails |
| plugin configs | readProjectConfig |
fails | fails | fails |
visibility never falls back: dropping a typo'd value would deploy and leave the app's access unchanged. A command that loads resources must not run on a guessed layout: entity push is a full sync, so defaulting entitiesDir or dropping plugins would delete the entities it no longer finds. build and publish still default a wholly missing config through resolveBuildTarget, as before.
Each fallback is reported once per command through setConfigWarningHandler, which Base44Command wires to log.warn. The site-only commands (site install, site dev, site deploy, build, publish) also use DEFAULT_SITE via siteOrDefault when a config has no site block. base44 dev and base44 deploy do not: there an absent block means "no frontend".
The base44 deploy command deploys all project resources in one operation:
import { deployAll, hasResourcesToDeploy } from "@/core/project/index.js";
if (!hasResourcesToDeploy(projectData)) {
return;
}
const { appUrl } = await deployAll(projectData);What it deploys (in order):
- App visibility, when configured
- Entities (via
entityResource.push()) - Functions (via
deployFunctionsSequentially()) - Actors (via
deployActorsSequentially()) - Agent skills (via
agentSkillResource.push()) - Agents (via
agentResource.push()) - Auth config (via
authConfigResource.push()) - Connectors (via
pushConnectors(), which only adds and updates; see explicit-delete) -- may return OAuth redirect URLs. A connector that fails to sync fails the deploy withResourceDeploymentError, but only after the site has shipped - Site (if
site.outputDirectoryis configured) — the legacy tar.gz upload. The deployments-API transport is not reachable from here; see deployments.md.
Function and actor batches collect each item's result. deployAll() must inspect
those results: any failure raises ResourceDeploymentError before the next stage.
Its details retain completed work and per-item outcomes for human and JSON error
output. Earlier successful deployments are not rolled back.
base44 deploy # With confirmation prompt
base44 deploy -y # Skip confirmation
base44 deploy --yes # Skip confirmation