# 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/`: ```bash 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 ```bash 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`: ```bash 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 '' dist/index.js`. ### 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/`) 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 ` 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](../api-reference/trigger-types.md#6-scheduledtime--on-a-schedule). ### 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](../SKILL.md#3-key-constraints). ## Test Infrastructure Automation E2E tests live in `scripts/automation-test/`: ```bash 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 ```bash # Count workflows by active state PGPASSWORD= 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= 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= 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;" ```