Skip to content

Project structure and configuration ​

Layout ​

A Cell project is an npm package. Its source follows a few conventions; nothing has to be registered elsewhere.

PathWhat
src/node/Backend code: controllers, services, middleware
src/browser/Frontend code (when the project has a frontend)
src/common/Code both sides import, such as RPC interfaces
src/node/module.tsThe backend DI module
src/browser/module.tsThe frontend DI module
cell.yml, cell.<mode>.ymlApplication configuration
cell.config.ts (optional)Toolchain configuration
.cell/Generated: generated/ (DI wiring, merged configuration) and output/ (the build); add it to .gitignore

DI modules ​

A DI module is a file that imports the files declaring components and exports autoBind():

ts
// src/node/module.ts
import './todo-controller';
import './todo-service';
import { autoBind } from '@celljs/core';

export default autoBind();

autoBind() registers every class decorated so far (@Component, @Controller, @Rpc, ...). A file that no module imports is never loaded: if cell check reports CELL_DI_NO_BINDING for a class you wrote, import its file here.

The toolchain looks for these module files of the project, in this order, and loads the ones that exist:

TargetFiles (under src/, extension .ts, .tsx, .js or .mjs)
backendcommon/module, node/backend-module, node/module, module, backend-module
frontendcommon/module, browser/frontend-module, browser/module, module, frontend-module

src/common/module and src/module are loaded on both sides. A package can declare other paths in the cell.modules field of its package.json.

Components ​

Every dependency whose package.json has a cell field is a component: installing it adds its DI modules and its cell.yml to the application. Installing @celljs/security turns on login, installing @celljs/serve-static serves the frontend. To leave out a component that a dependency brings in, list it in cell.config.ts:

ts
export default defineConfig({ modules: ['!@celljs/serve-static'] });

npx cell info lists the components, their DI modules, the entry and the preset Cell resolved; --json gives the same as data.

Application configuration: cell.yml ​

yaml
# yaml-language-server: $schema=./.cell/schema/cell.yml.json
server:
  port: ${env.PORT ?? 8080}      # keys of the components are at the top level
typeorm:
  ormConfig:
    - type: mysql
      password: ${env.DB_PASSWORD}
app:                              # your own configuration goes under app
  greeting: Hello
  public:                         # only app.public reaches the browser
    apiBase: /api
  • Top-level keys are configuration keys; there is no cell:, frontend: or backend: level. Read them with @Value('server.port'), @Value('app.greeting') on a property or constructor parameter.
  • Every top-level key has an owner: a component declares the keys it owns in its cell.schema.json (server belongs to @celljs/web, security to @celljs/security...). Writing under them overrides the component's defaults.
  • Your own configuration goes under app, which no component takes, so an upgrade never collides with it.
  • Mistakes are errors: a top-level key nobody owns, an option a component does not have, a wrong type — cell build, cell dev and cell check report CELL_CONFIG_INVALID with the file, the line and a "did you mean".
  • cell dev, cell build and cell check write .cell/schema/cell.yml.json; with the yaml-language-server comment above as the first line of cell.yml, editors (the YAML extension of VS Code, ...) complete and check it.

What the frontend sees ​

Configuration is server-only by default. The browser only receives the keys components mark public (such as server.path, rpc, react, the page redirects of security) and app.public. A secret never reaches the frontend bundle unless you put it under app.public.

  • Frontend values are evaluated at build time; their expressions may refer to server keys, only the results ship.
  • npx cell config get <key> --target frontend shows what the frontend gets; a server-only key is reported as such.
  • A project may have its own cell.schema.json describing the keys under app (types, descriptions, "x-cell-public": true).

Merge order ​

The files merge in this order: the cell.yml of every component (in dependency order), the project's cell.yml, then cell.<mode>.yml for each active mode. Later files win; objects merge deeply, arrays are replaced.

Modes ​

CommandMode
cell devdevelopment
cell buildproduction
createTestApp()test

Add more with -m <mode> (repeatable): cell build -m staging also reads cell.staging.yml. A mode without any file is reported by cell check (CELL_MODE_UNKNOWN).

Expressions ​

${...} is evaluated against the configuration itself plus env (the process environment):

yaml
server:
  endpoint: http://localhost:${server.port}     # another key
cloud:
  region: ${env.REGION ?? 'eu-west-1'}          # environment variable with a default
typeorm:
  ormConfig:
    - port: ${env.DB_PORT ?? 5432}              # the schema says integer: DB_PORT=6543 gives the number 6543
      synchronize: ${env.DB_SYNC ?? false}      # the schema says boolean: true or false only
  • Defaults use ??, as in JavaScript; an empty environment variable counts as unset. Values need no quotes.
  • Schema types convert: when a key is a number or a boolean, the text an expression gives is converted, and a value that does not convert is an error naming the key. Keys without a schema (those under app) keep the type of the result.
  • Backend values are evaluated when the server starts, so one build runs in every environment and secrets are not baked into it. Frontend values are evaluated at build time.
  • An expression inside other text gives a string; the result of an expression is never evaluated again, even when an environment variable contains ${...}.

Environment variables ​

Override any existing key without editing files, with a variable starting with CELL_:

bash
CELL_SERVER__PORT=8080 node .cell/output/server/index.mjs   # server.port, applied at startup
CELL_SERVER__HOST=0.0.0.0                                    # listen on all interfaces (containers)
CELL_APP__PUBLIC__API_BASE=https://api.example.com npx cell build   # app.public.apiBase of the frontend, at build time
  • One underscore after the prefix, __ between levels (CELL_SERVER__PORT). CELL_PRESET, CELL_MODE, CELL_EVENT, CELL_OUT_DIR and CELL_DEBUG belong to the toolchain and are never configuration; a one-segment variable that is no top-level key (another tool's CELL_REGION) is left alone; a name with __ that matches no key is reported, with the right spelling for the earlier CELL__… / CELL_BACKEND__….
  • __ separates levels, matched ignoring case, _ and - (ORM_CONFIG matches ormConfig); array items by index.
  • The same variables apply to both sides: the server applies them at startup; cell build applies them, then picks the public keys for the frontend.
  • Only existing keys can be overridden; give a new key a default in cell.yml first.
  • Values convert to the type of the value (or the schema): numbers, booleans, JSON for objects and arrays. A value that is one ${...} is kept and evaluated: CELL_SERVER__PORT='${env.PORT}'.
  • A variable that matches no key or does not fit its type is ignored with a warning; cell check and cell info report it.

Where a value comes from ​

bash
npx cell config get server.port --why
# server.port (backend) = 8080
#   server-only, owned by @celljs/web
#   Port the server listens on. Override with CELL_SERVER__PORT.
#   node_modules/@celljs/web/cell.yml: 3000
#   cell.yml: 8080

Common keys of the framework:

KeyDefaultPackage
server.port / server.host3000 / 127.0.0.1@celljs/web
server.path/@celljs/web (prefix of every route; public)
web.body.limit100kb@celljs/web
web.corsany origin (*)@celljs/web
web.session / web.cookiessee the README of @celljs/web@celljs/web
html.title / html.rootIdCell Framework / cell-root@celljs/core (the generated HTML page)
logger.levelinfo@celljs/core
rpc.pathapi@celljs/rpc
serveStatic.spatrue@celljs/serve-static

Each package's README lists its keys; every key with its type and description is in .cell/schema/cell.yml.json.

Toolchain configuration: cell.config.ts ​

Optional, and only for the toolchain. Application configuration never goes here.

ts
import { defineConfig } from '@celljs/cli';

export default defineConfig({
    preset: 'cloudflare',                          // default: node
    modules: ['!@celljs/serve-static'],            // remove a component
    build: { external: ['sharp'], sourcemap: true },
    vite: { resolve: { alias: { '~': '/src' } } }, // merged into the Vite configuration
    hooks: { 'build:before': 'prisma generate' },  // shell commands on toolchain events
    fc: { region: 'cn-shanghai', memory: 512 },    // options of a preset, under its name
});

Unknown fields are reported with the closest valid name (CELL_CONFIG_INVALID, CELL_CONFIG_UNKNOWN_OPTION). The full reference, including toolchain modules that add Vite plugins, checks, MCP tools or output files, is the CLI reference.

Generated files ​

cell dev and cell build write the code they generate to .cell/generated/: the DI modules each target loads, the merged configuration and the project model. Read them when you need to know exactly what the container contains; never edit them.