Files
yeuimu 76ef02d659 refactor: move skill to skills/inkreach-teable (from .agents/skills/)
git mv rename (history preserved); install.sh + README updated.
The skill is consumed via user-level install (~/.zcode/skills), so
in-repo .agents discovery was unnecessary.
2026-09-23 04:07:39 +00:00

8.2 KiB

Development & Deployment

This guide documents the inkreach-teable application repo (ssh://git@gitea.inkreach.cc:2222/inkreachhub/inkreach-teable.git) — the deployed instance this CLI operates on — not this CLI repo. Paths like apps/nestjs-backend/ refer to that repo's checkout.

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