Skip to content

Setup

Alchemify runs on Node.js and PostgreSQL. No Docker required.

  • PostgreSQL 14+ installed and running
  • psql command available
  • Node.js 20+, pnpm

Connect as a PostgreSQL superuser (typically postgres):

Terminal window
psql -U postgres -c "CREATE DATABASE alchemify"

The schema creates all roles, tables, RLS policies, and auth functions. It is idempotent — safe to re-run. First generate the page SQL, then apply the schema:

Terminal window
cd apps/server && bash seed-pages.sh
psql -U postgres -d alchemify -f apps/server/schema.sql

This creates:

RolePurpose
authenticatorLogin role for the connection pool. Cannot access data directly.
ownerFull access (identical to admin at DB level; org-management distinction is app-level)
adminFull access to all data
staffOrg-scoped access (limited by RLS)
memberRead-only access to business data
anonUnauthenticated access (very limited)

The authenticator role is granted the ability to SET ROLE to the five app roles. The server uses this to impersonate the appropriate role per request.

Terminal window
cp apps/server/.env.example apps/server/.env

The default .env connects as authenticator with the password from apps/server/schema.sql:

DATABASE_URL=postgres://authenticator:authenticator_password_change_me@localhost:5432/alchemify
JWT_SECRET=dev-secret-change-in-production
LOG_LEVEL=debug

Most settings (port, JWT expiration, file upload limits, etc.) have sensible defaults defined in apps/server/src/config.ts. LOG_LEVEL controls structured logging output (see Logging). For local development the defaults work as-is. For anything shared, change the authenticator password in both apps/server/schema.sql and .env.

Terminal window
# Confirm authenticator can connect
psql "postgres://authenticator:authenticator_password_change_me@localhost:5432/alchemify" \
-c "SELECT current_user"
# Confirm role switching works
psql "postgres://authenticator:authenticator_password_change_me@localhost:5432/alchemify" \
-c "SET ROLE 'anon'; SELECT current_user"

Both should succeed. If the first fails, check that the authenticator role exists and the password matches.

Terminal window
pnpm install
pnpm dev

Verify with:

Terminal window
curl -s http://localhost:3000/health
# {"status":"ok"}

Tests run against the same alchemify database. They truncate tables between runs.

Terminal window
pnpm test

The project uses ESLint and Prettier. Run them to verify code quality:

Terminal window
pnpm lint # check for lint errors
pnpm format:check # verify formatting
pnpm format # auto-fix formatting

If you prefer Docker for local PostgreSQL, a single command gives you a fully initialized database:

Terminal window
docker compose up

This starts a postgres:16 container, creates the alchemify database, and applies all schema files automatically on first startup. By default this includes chat-schema.sql; to skip it, set INCLUDE_CHAT: "false" in the postgres environment section of docker-compose.yml.

The default DATABASE_URL in .env.example works without changes:

DATABASE_URL=postgres://authenticator:authenticator_password_change_me@localhost:5432/alchemify

Notes:

  • Initialization runs only on first startup (when the data volume is empty). Subsequent docker compose up calls reuse the existing data.
  • If you have a local PostgreSQL already running on port 5432, stop it first or you’ll get a port conflict.
  • To reset the database and start fresh:
Terminal window
docker compose down -v && docker compose up

After the container is running, continue from step 3 above.

If you’re running inside a container where systemd is not available, these extra steps are needed.

systemctl won’t work — use pg_ctlcluster directly:

Terminal window
sudo pg_ctlcluster 14 main start
sudo pg_ctlcluster 14 main stop
sudo pg_ctlcluster 14 main restart

The default pg_hba.conf uses peer auth, which requires matching the OS user to the PostgreSQL user. You can’t run psql -U postgres as a non-postgres OS user.

Fix: change local auth from peer to md5 in pg_hba.conf:

/etc/postgresql/14/main/pg_hba.conf
# Find the file
sudo -u postgres psql -c "SHOW hba_file"
# Edit it — change "peer" to "md5" on the local lines (keep peer for postgres user)
sudo vi /etc/postgresql/14/main/pg_hba.conf
# Restart PostgreSQL
sudo pg_ctlcluster 14 main restart

After this, psql -U postgres works directly (with password prompt) — no more sudo -u postgres prefix.

Alternative (if you don’t want to edit pg_hba.conf): prefix commands with sudo -u postgres:

Terminal window
sudo -u postgres psql -c "CREATE DATABASE alchemify"

When running sudo -u postgres from a directory the postgres user can’t access (like /home/user/...), you get:

could not change directory to "/home/user/project": Permission denied

This is a warning, not an error — the command still runs. But file paths passed to -f must be accessible to the postgres user.

Workaround: copy the schema to a world-readable location:

Terminal window
cp apps/server/schema.sql /tmp/schema.sql
sudo -u postgres psql -d alchemify -f /tmp/schema.sql

Rather than using sudo -u postgres for everything, create a dedicated PostgreSQL user with full access and a .pgpass file for passwordless login:

Terminal window
# As postgres superuser:
sudo -u postgres psql <<'SQL'
CREATE USER dba WITH PASSWORD 'your-password-here';
GRANT ALL PRIVILEGES ON DATABASE alchemify TO dba;
\c alchemify
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO dba;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO dba;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO dba;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO dba;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO dba;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO dba;
SQL

Then set up passwordless access:

Terminal window
echo "localhost:5432:alchemify:dba:your-password-here" >> ~/.pgpass
chmod 600 ~/.pgpass

Now you can connect directly: psql -U dba alchemify

Terminal window
# 1. Start PostgreSQL (no systemd)
sudo pg_ctlcluster 14 main start
# 2. Change pg_hba.conf local auth from peer to md5 (keep peer for postgres user)
sudo vi /etc/postgresql/14/main/pg_hba.conf
sudo pg_ctlcluster 14 main restart
# 3. Create the database
sudo -u postgres psql -c "CREATE DATABASE alchemify"
# 4. Apply schema (copy to accessible path first)
cp apps/server/schema.sql /tmp/schema.sql
sudo -u postgres psql -d alchemify -f /tmp/schema.sql
# 5. Create admin user (see "Admin user for dev" above)
# 6. Set authenticator password (must match .env)
psql -U dba alchemify -c "ALTER ROLE authenticator WITH PASSWORD 'authenticator_password_change_me'"
# 7. Verify
psql -U dba alchemify -c "SELECT current_user"

ECONNREFUSED 127.0.0.1:5432 — PostgreSQL is not running. Start it:

Terminal window
# Linux (systemd)
sudo systemctl start postgresql
# macOS (Homebrew)
brew services start postgresql
# Container (no systemd)
sudo pg_ctlcluster 14 main start

role "authenticator" does not exist — Schema hasn’t been applied. Re-run step 2.

password authentication failed for user "authenticator" — Password mismatch between .env and the role in PostgreSQL. Either re-apply apps/server/schema.sql (which uses IF NOT EXISTS and won’t reset an existing password) or alter it manually:

ALTER ROLE authenticator WITH PASSWORD 'authenticator_password_change_me';

permission denied for table users — You connected as authenticator directly. This is expected — authenticator has no table access. The server calls SET LOCAL ROLE to switch to an app role before querying.

Re-applying the schemaapps/server/schema.sql uses IF NOT EXISTS for roles/tables and CREATE OR REPLACE for functions, so it is safe to re-run at any time. However, CREATE POLICY will error if a policy already exists. To do a clean reset:

Terminal window
psql -U postgres -c "DROP DATABASE alchemify"
psql -U postgres -c "CREATE DATABASE alchemify"
psql -U postgres -d alchemify -f apps/server/schema.sql

Container: pg_ctlcluster start warning — The “connection to the database failed” warning after switching to md5 auth is harmless — the startup check can’t authenticate, but the server starts fine.

Container: NOTICE messages during schema apply — Messages like “role already a member” or “trigger does not exist, skipping” are harmless — the schema uses IF NOT EXISTS and DROP ... IF EXISTS.