Skip to content

Latest commit

 

History

History
200 lines (139 loc) · 12.4 KB

resources.md

File metadata and controls

200 lines (139 loc) · 12.4 KB

Working with Resources

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.

Resource Interface

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).

Removing remote items: explicit-delete vs mirrored

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.

Resource Implementation

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,
};

Client-side validation must match server-side validation

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/.

Adding a New Resource

  1. Create folder: packages/cli/src/core/resources/<name>/
  2. Add schema.ts with Zod schemas
  3. Add config.ts with file reading logic
  4. Add resource.ts implementing Resource<T>
  5. Add api.ts for API calls
  6. Add index.ts barrel exports
  7. Update packages/cli/src/core/resources/index.ts to export the new resource
  8. Register in packages/cli/src/core/project/config.ts (add to readProjectConfig)
  9. Add typed field to ProjectData interface

Backend functions (project layout)

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.

Entry file contract

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 a Request, returns a Response.

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.

Actor integration

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

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.

Workflow Module (Read-Only, Not a Resource)

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.

Site Module (Not a Resource)

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.outputDirectory and upload via POST /api/apps/{app_id}/deploy-dist. It tars site.outputDirectory, or ./dist when 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.

Deploy Flow

  1. Validate output directory exists and has files
  2. Create temporary tar.gz archive using tar package
  3. Upload archive to the API
  4. Parse response with Zod schema
  5. Clean up temporary archive file

Config fallback

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".

Unified Deploy Command

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):

  1. App visibility, when configured
  2. Entities (via entityResource.push())
  3. Functions (via deployFunctionsSequentially())
  4. Actors (via deployActorsSequentially())
  5. Agent skills (via agentSkillResource.push())
  6. Agents (via agentResource.push())
  7. Auth config (via authConfigResource.push())
  8. 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 with ResourceDeploymentError, but only after the site has shipped
  9. Site (if site.outputDirectory is 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