The backend never compiles @teable/* sources — it loads packages/*/dist via node_modules symlinks (@teable/v2-core is a webpack external, read at runtime from packages/v2/core/dist/index.cjs). Editing a package's src and only rebuilding apps/nestjs-backend ships nothing: no error, old behavior online. Documented the correct build order + verification greps in AGENTS.md and the skill's development guide (found while shipping the defaultValue cascade fix).
7.9 KiB
Development & Deployment
How to build, restart, and debug this inkreach-teable instance. Covers the
dual-process architecture, known gotchas, and test infrastructure. For daily
operations (tables/automation), use the inkreach-teable CLI instead — this guide is
for changing the source code itself.
Architecture
Two separate production processes managed by systemd user services. Do NOT
use pnpm dev as a long-running service — the embedded Next.js dev server
inside NestJS is unstable and will hang or OOM-crash.
| Service | Command | Port | Mode |
|---|---|---|---|
teable-backend.service |
node dist/index.js |
:3003 |
NODE_ENV=production, BACKEND_SKIP_NEXT_START=true |
teable-frontend.service |
next start -p 3002 -H 0.0.0.0 |
:3002 |
NODE_ENV=production |
Users access :3002; the backend :3003 serves the API only.
Build & Restart
Backend (no hot reload)
After editing anything under apps/nestjs-backend/src/:
cd apps/nestjs-backend && pnpm build # webpack, ~18s
systemctl --user restart teable-backend.service
dist/ from pnpm dev:swc is hot-update fragments, NOT a valid artifact — always nest build.
Frontend
pnpm -r -F './packages/**' build # build dependent packages first
pnpm --filter @teable/app build-fast
systemctl --user restart teable-frontend.service
Browser cache is aggressive — hard-refresh (Ctrl+Shift+R) after a rebuild.
Recover from OOM crash
If :3003 is unreachable but systemd says active:
systemctl --user stop teable-backend.service
pkill -9 -f 'dist/index.js'
systemctl --user start teable-backend.service
Known Gotchas (from real debugging)
Webpack cache may not rewrite dist/index.js after a workspace package rebuild
After rebuilding a workspace package (e.g. @teable/openapi dist), pnpm build in
apps/nestjs-backend can report "compiled successfully" while leaving dist/index.js
untouched (node_modules paths are treated as immutable cache inputs). Symptom: backend
behavior unchanged after a code change. Fix: rm apps/nestjs-backend/dist/index.js
then rebuild, and verify with grep '<your-new-string>' dist/index.js.
Workspace packages must be rebuilt BEFORE the backend (else stale dist ships)
Distinct from the cache trap above: apps/nestjs-backend never compiles @teable/*
sources — it resolves @teable/core, @teable/v2-core, @teable/openapi … via
node_modules symlinks to packages/*/dist. @teable/v2-core in particular is a
webpack EXTERNAL: the running backend reads packages/v2/core/dist/index.cjs directly
at runtime, so a backend rebuild does not even copy it.
SILENT FAILURE MODE (hit 2026-08-18, select-defaultValue cascade fix): edit
packages/core/src/… + packages/v2/core/src/… → only rebuild backend + restart →
live API keeps the old behavior, no error anywhere. Correct order:
pnpm --filter @teable/core build # tsc → packages/core/dist/**
pnpm --filter @teable/v2-core build # tsdown → dist/index.cjs + dist/index.js
cd apps/nestjs-backend && rm -f dist/index.js && pnpm build # bundle + externals
# verify the new code actually shipped BEFORE restarting:
grep '<new-string>' packages/core/dist/<path>.js
grep '<new-string>' packages/v2/core/dist/index.cjs
# then the best-path restart (:3003)
(Many packages at once: pnpm -F './packages/**' build, then backend build.)
Node v24 fetch drops the Authorization header
The global fetch() on Node v24 (undici) drops the Authorization request
header in certain cases (redirects, proxies). Use node:http/node:https
directly for any backend API call, or the axios-style clients. This is why
the inkreach-teable CLI uses node:http instead of fetch.
Script sandbox has no process global
In automation script actions, typeof process === 'undefined'. Access secrets
via process.env.NAME (this is allowed — only process.env is exposed, not
process itself). Use triggerInput._secrets, triggerInput._accessToken,
triggerInput._apiBaseUrl for back-channel API calls. No fs, eval,
Function, or child_process.
Script sandbox npm deps need npm on the service PATH
Script actions CAN use npm packages — declare them via the CLI
(action add/update --dependencies '[{"name":"axios","version":"1.6.0"}]'
or shorthand axios@1.6.0,dayjs) and require('axios') works in-sandbox.
The backend installs deps into a cache dir (/tmp/teable-automation-deps/<hash>)
via spawn('npm', ['install', ...]), keyed by name@version.
Gotcha: systemd user services default to PATH=/usr/local/bin:/usr/bin:...
which OMITS the nvm bin dir where npm lives → scripts with deps fail with
Dependency install failed: spawn npm ENOENT. Fix: add
Environment=PATH=/root/.nvm/versions/node/vX/bin:/usr/local/sbin:... to
teable-backend.service, daemon-reload, restart. Verify:
inkreach-teable workflow test <wfId> on a script with require('dayjs') →
succeeded.
scheduledTime backend is a simplified implementation
The backend scheduled-time.trigger.ts only honors timing.type + timing.interval.
It ignores triggerTime, weekdays, daysOfMonth, starting, ending, tz.
weeks type is hardcoded to every Sunday. OneTime falls back to every 60s.
The scheduler is in-process setInterval — multi-instance deployments fire
duplicates. See trigger-types.md § scheduledTime.
Webhook route mismatch
Frontend UI shows /api/webhook/base/{baseId}/workflow/{workflowId} but the
backend's real route is /api/webhook/{baseId}/{workflowId} (no /base/ or
/workflow/ segments). The CLI uses the real route. Webhook authorization
config is UI-only — backend does NOT validate it.
Button field options capture stale isActive
A Button field's options.workflow.isActive is a snapshot from creation time.
After deactivating a workflow, the API validates the DB is_active instead —
a click returns 400 "not active". Re-activate the workflow, or recreate the
field. Always verify DB state, not field options.
base_node pollution (sidebar perf)
Workflows create entries in the base_node table, which feeds the sidebar
navigation tree. Hundreds of test workflows → thousands of DOM nodes → jank.
The sidebar is now virtualized (@tanstack/react-virtual), but avoid
generating throwaway workflows; deactivate instead of delete to keep them
queryable.
Key-name differences from official teable
The self-built automation uses conditions (not filter), fieldIds (not
watchFieldIds), category recordCondition (not recordMatchesConditions).
The inkreach-teable CLI handles these correctly; never use the official teable
CLI for automations. See SKILL.md § Key Constraints.
Test Infrastructure
Automation E2E tests live in scripts/automation-test/:
node scripts/automation-test/run-all.js # 59 cases: 8 triggers + 7 actions + cross-features
node scripts/stress-test/stress-test.js # concurrency stress test
Lib framework: scripts/automation-test/lib.js. Uses Node http (not fetch)
due to the auth-header bug. Test workflows are deactivated, not deleted
(retention policy) — the base has ~600 historical test workflows.
Useful DB Queries
# Count workflows by active state
PGPASSWORD=<ask-maintainer> psql -h localhost -U teable -d teable -c \
"SELECT is_active, count(*) FROM workflow WHERE deleted_time IS NULL GROUP BY is_active;"
# Find button fields and their bound workflows
PGPASSWORD=<ask-maintainer> psql -h localhost -U teable -d teable -c \
"SELECT id, name, options->'workflow'->>'id' AS wf_id, options->'workflow'->>'isActive' AS active
FROM field WHERE type='button';"
# Recent executions
PGPASSWORD=<ask-maintainer> psql -h localhost -U teable -d teable -c \
"SELECT id, workflow_id, status, started_at FROM workflow_execution
ORDER BY started_at DESC LIMIT 10;"