sc-dev-deploy/docs/operations.md
2026-06-01 16:43:43 -05:00

9.1 KiB

dev-deploy operations

Running and maintaining the local development instances for the dev-deploy plugin. Three Saltcorn instances back development and testing: a SQLite MAIN instance on :3000 (which also hosts the saltcorn-idp LDAPS listener), a SQLite TEST instance on :3001, and a Postgres multi-tenant instance on :3002.

See also: architecture.md, peering.md, multitenancy.md, testing.md.

Contents

Layout

The project root is ~/claude/saltcorn. The dev-deploy plugin source lives in the dev-deploy/ subfolder; the saltcorn-idp plugin source is a sibling in idp/; upstream Saltcorn is checked out under saltcorn/. Each instance keeps its database, file store, and session store under its own state directory in the project root:

Instance Port State dir Database LDAPS port
MAIN 3000 .dev-state/ SQLite (saltcorn.sqlite) 1636
TEST 3001 .dev-state-test/ SQLite (saltcorn.sqlite) none
PG (multi-tenant) 3002 .dev-state-pg/ Postgres saltcorn_idp 1637

The start scripts cd into the state dir before running saltcorn serve so each instance writes its own sessions.sqlite (Saltcorn's SQLite session store writes sessions.sqlite at the process cwd, per packages/server/routes/utils.js).

The three dev instances

MAIN (:3000, SQLite)

startServer.sh sources .dev-state/env.sh, runs saltcorn install-plugin -d ./dev-deploy (non-fatal on failure -- the previously installed version still loads), then cd .dev-state and exec saltcorn serve "$@". MAIN is the only instance that sets SALTCORN_IDP_LDAP_PORT=1636, so saltcorn-idp opens its LDAPS listener there. The other instances leave that port unset (PG sets 1637; see below), so the three do not collide. saltcorn-idp is installed via reinstallIdp.sh rather than in the start script (the shared plugins_folder makes a per-boot install race on concurrent starts); it loads at serve time from that install.

TEST (:3001, SQLite)

startServerTest.sh sources .dev-state-test/env.sh (which sets SALTCORN_PORT=3001), installs the plugin the same way, then cd .dev-state-test and exec saltcorn serve -p "$SALTCORN_PORT" "$@". TEST is the promote/pull peer used by the e2e suite. It does not set SALTCORN_IDP_LDAP_PORT, so no LDAP listener runs there.

PG (:3002, Postgres multi-tenant)

startServerPg.sh sources .dev-state-pg/env.sh, then cd .dev-state-pg and exec saltcorn serve -p 3002 "$@". The PG env deliberately does NOT set SQLITE_FILEPATH, so getConnectObject() selects Postgres; it sets SALTCORN_MULTI_TENANT=true to enable schema-per-tenant (Postgres only). Unlike the SQLite start scripts, startServerPg.sh does NOT run install-plugin at boot -- the plugin is installed into the public schema by reinstallDevDeploy.sh and activated per tenant by installDevDeployTenant.sh (see below). After per-tenant activation, each tenant's onLoad re-runs automatically on every boot via init_multi_tenant -> loadAllPlugins. The PG env also sets SALTCORN_IDP_LDAP_PORT=1637 (distinct from MAIN's 1636) so the multi-tenant LDAP gate can exercise tenant-in-DN binds against this instance.

Environment files

Each state dir has an env.sh that is sourced before running saltcorn. They all prepend the in-tree CLI (saltcorn/packages/saltcorn-cli/bin) to PATH so saltcorn ... resolves to this checkout, and load nvm.

Variable MAIN TEST PG
SQLITE_FILEPATH .dev-state/saltcorn.sqlite .dev-state-test/saltcorn.sqlite unset (selects Postgres)
SALTCORN_FILE_STORE .dev-state/files .dev-state-test/files .dev-state-pg/files
SALTCORN_SESSION_SECRET set set set
SALTCORN_PORT unset (defaults to 3000) 3001 unset (passed -p 3002)
SALTCORN_IDP_LDAP_PORT 1636 unset 1637
SALTCORN_MULTI_TENANT unset unset true
SALTCORN_JWT_SECRET unset unset set
PGHOST / PGUSER / PGDATABASE / PGPASSWORD unset unset /var/run/postgresql / scott / saltcorn_idp / peer

On PG, Saltcorn only selects Postgres when user, password, and database are all set (connect.ts getConnectObject). Authentication is via the Unix socket with peer auth, which ignores the password, so PGPASSWORD=peer is a dummy that just satisfies that check.

Starting the instances

Run from the project root:

cd ~/claude/saltcorn
./startServer.sh &        # MAIN :3000 (+ LDAPS :1636)
./startServerTest.sh &    # TEST :3001
./startServerPg.sh &      # PG multi-tenant :3002 (+ LDAPS :1637)

The two SQLite start scripts each attempt install-plugin -d ./dev-deploy at boot (failures are non-fatal). The PG instance does not; install it explicitly as described below.

Installing the plugin

After editing the plugin source, reinstall it into all three instances with the servers stopped:

cd ~/claude/saltcorn
./reinstallDevDeploy.sh

reinstallDevDeploy.sh installs dev-deploy into MAIN (.dev-state), TEST (.dev-state-test), and the PG public schema (.dev-state-pg). It is a separate script (not folded into the start scripts) because the saltcorn plugins_folder (~/.local/share/saltcorn-plugins) is shared by all instances, and saltcorn install-plugin:

  • needs an ABSOLUTE -d path (it path.join()s then require()s, so a leading ./ is collapsed and resolved as a node module), and
  • aborts (EEXIST) if the per-plugin-dir node_modules symlinks already exist.

Doing this in each start script would race when instances boot concurrently, so reinstalls are centralized here. Before each install the script clears the node_modules symlinks under the plugins root so install-plugin can recreate them cleanly.

For the PG instance, installing into the public schema is only step one; you must then activate the plugin per tenant (next section).

Per-tenant install on Postgres

The public-schema install does not create the _dd_* tables in each tenant schema. To register + enable dev-deploy in a tenant schema and run its onLoad (creating the _dd_* tables and bootstrapping the env row), use:

./dev-deploy/scripts/installDevDeployTenant.sh t1 t2     # named tenants
./dev-deploy/scripts/installDevDeployTenant.sh '*'       # all tenants

The shell wrapper resolves the project root, sources .dev-state-pg/env.sh, and runs installDevDeployTenant.js. The JS uses Saltcorn's supported Plugin.loadAndSaveNewPlugin inside runWithTenant (replacing the old manual INSERT INTO <tenant>._sc_plugins SQL hack). It sets the root-only config tenants_unsafe_plugins=true so a LOCAL plugin can be installed into tenant schemas, converges each tenant to exactly one _sc_plugins row, and verifies the install by confirming both the _sc_plugins row and the tenant-schema _dd_env table exist. With no args or a single *, it installs into every tenant from the public _sc_tenants list.

Prerequisites:

  • the tenants must already exist (saltcorn create-tenant <name>), and
  • the plugin must be installed into the PG public schema once (the reinstallDevDeploy.sh path) so the shared plugins_folder copy exists.

After per-tenant activation, each tenant's onLoad re-runs automatically on every boot of startServerPg.sh. The SQLite MAIN/:3000 and TEST/:3001 instances are unaffected (they set SQLITE_FILEPATH). See multitenancy.md for the schema-qualification details.

Known issues

The node_modules symlinks under the plugins root (~/.local/share/saltcorn-plugins) are SHARED across plugins. The clearSymlinks() step in reinstallDevDeploy.sh therefore also clears saltcorn-idp's symlinks. After running reinstallDevDeploy.sh, you MUST also re-run reinstallIdp.sh (and vice versa) to keep both plugins' installs consistent, then restart the servers. reinstallIdp.sh installs saltcorn-idp into MAIN and TEST only (it does not touch the PG instance).

Recommended sequence after editing plugin source:

# stop the servers first
./reinstallDevDeploy.sh
./dev-deploy/scripts/installDevDeployTenant.sh '*'   # PG only, if using tenants
./reinstallIdp.sh
# restart the servers

Themeless freshly-created tenant returns 500 from sendWrap

A freshly created Postgres tenant has no theme/layout configured, so Saltcorn's own admin pages that call res.sendWrap (for example GET /table/new/) return HTTP 500. dev-deploy's admin pages self-render and are unaffected. When you need a CSRF token for a Saltcorn admin POST against a themeless tenant, grab the token from a dev-deploy page (for example GET /admin/dev-deploy/peers) instead -- the mutate-then-redirect POST (such as POST /table) does not call sendWrap and so succeeds. This is exploited throughout the PG gates; see testing.md.