- action add/update: new --dependencies <json> option - parseDependencies accepts JSON, name@version shorthand, or bare names - update is incremental; --dependencies replaces the whole array - docs: deployment/systemd.md + skill SKILL/action-types/development document the npm-deps feature and the service-PATH requirement (spawn npm ENOENT without nvm bin dir on PATH)
6.2 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 |
:3000 |
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 :3000 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 :3000 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)
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 + 6 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=teable 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=teable 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=teable 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;"