# Constructive Troubleshooting

> Constructive platform failure recipes: exact error strings mapped to root causes and fixes across provisioning, blueprints, RLS, auth, codegen, and frontend. Load when a build step or verify fails.

- Skill: `constructive-io/constructive-troubleshooting` (Agent Skill)
- Install (CLI): `npx skillmds@latest add constructive-io/constructive-troubleshooting`
- Raw SKILL.md: https://api.skillmd.com/api/skills/constructive-io/constructive-troubleshooting/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: constructive-io (https://skillmd.com/u/constructive-io)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/constructive-io/constructive-troubleshooting

---


# Troubleshooting Guide

When you run into issues, read this document for known problems and solutions.

---

## Quick Index

> **Tip:** know the exact error string but not the phase? Use the `constructive-error-index` skill where available — a flat `symptom string → cause → fix pointer` lookup (gotcha CODE / this guide's section) that routes you straight to the authoritative entry.

- **General**
  - Docker Postgres issues
- **Phase 1**
  - GraphQL Server not responding
  - GraphQL Server returns HTML "Not Found"
  - Agent stuck on nohup + curl verification (hang)
- **Post-Provision** (Email Services)
  - Mailpit not running
  - Admin GraphQL Server (3002) hangs on startup
  - send-email-link not sending emails
  - "Missing site configuration for email" error
  - job-service not processing jobs
- **Phase 2.1**
  - `pgpm init` non-interactive mode fails
- **Phase 2.3**
  - `__dirname` undefined in ESM scripts
  - Workaround schema name has hash (query real schema name with psql first)
  - `constructBlueprint` fails `NOT_FOUND (memberships_module)` (AuthzEntityMembership on an org-less app — use AuthzDirectOwner/AuthzAllowAll)
- **Phase 2.4** (Optional - skip in most cases, boilerplate includes codegen)
  - SDK build missing dependencies
  - `makage` missing `tsconfig.esm.json` / `README.md`
  - `pgpm init` fails in non-packages directory
  - Recommend skipping CLI by default (generate schema + sdk only)
- **Phase 2.5** (Boilerplate only)
  - `pgpm init -w` hangs (waiting for input)
  - Next.js 500 after start (missing `@sdk/*` / generated output)
- **Phase 3** (Per-DB integration + business UI)
  - updateUser returns 200 but does not persist (users-table UPDATE-policy gap — apply the createSecureTableProvision self_update step)
  - Auth hook onSuccess cannot get password (mutationFn must return password)
  - configure-app-sdk `headers` type error (must pass Record<string, string>, not a function)
  - Route structure (don't assume (authenticated) etc.; check template app/ structure first)
  - Don't wrap page in AppShell again (layout already has AuthenticatedShell; see existing users/page.tsx)
  - ORM/client delete missing select (must pass select explicitly)
  - orderBy enum values missing (CREATED_AT_DESC / POSITION_ASC etc.; use generated schema values or add index first)
  - No QueryClient set (SDK hooks vs app react-query instance; prefer generating SDK inside app)
  - No redirect after login / no Sidebar entry (router.push in onSuccess, add links in sidebar)
  - UI component import wrong (`@constructive-io/ui/*` vs template `@/components/ui/*`)
  - Use Stack when template has it (do not create Dialog for CRUD — check `ls .../ui/stack` first; see constructive-frontend / CRUD Stack)
  - SDK query result fields nullable (type errors in form/handler — use `?? ''` or accept nullable type)
  - Invalid UUID error on create/update (relation field `isRequired: false` missing)
  - Used confirm() or alert() for delete (must use template AlertDialog from `@/components/ui/alert-dialog`)
  - Next.js cannot find `@<app>/sdk/dist/...` (workspace path resolution)
  - Hooks argument error (mutation no input wrapper, query pass id directly; check generated hook @example / select vs selection.fields)

---

## General: Docker Postgres issues

### Problem

Postgres container is not running or connection fails.

### Solution

```bash
# Check container status
docker ps | grep postgres

# If not running, start it
eval "$(pgpm env)"
pgpm docker start

# Verify connection
psql -h localhost -U postgres -c "SELECT 1;"
```

---

## Phase 1: GraphQL Server not responding

### Problem

`curl` returns `000` or hangs (connection failure / timeout).

### Solution

1. Ensure the database is deployed:
```bash
eval "$(pgpm env)"
psql -c "SELECT datname FROM pg_database WHERE datname = 'constructive';"
```

2. If the database does not exist, deploy it:
```bash
cd /path/to/constructive-db
pgpm deploy --database constructive --createdb --yes --package constructive-services
pgpm deploy --database constructive --yes --package constructive-local
```

3. Start the GraphQL server:
```bash
cd /path/to/constructive/graphql/server
PGDATABASE=constructive pnpm dev
```

4. Wait for the server to be ready (**first start may take 20–30 seconds**):
```bash
sleep 25
```

5. Health check with POST + Host header + JSON body (expect GraphQL data):
```bash
curl -s --connect-timeout 5 --max-time 10 \
  -H "Host: auth.localhost:3000" \
  -H "Content-Type: application/json" \
  -X POST \
  http://localhost:3000/graphql \
  -d '{"query":"{ __typename }"}'
# Expected: {"data":{"__typename":"Query"}}
```

---

## Phase 1: GraphQL Server returns HTML "Not Found"

### Problem

GraphQL server process is running (`lsof -i:3000` shows node) but the request returns an HTML page instead of a GraphQL response:

```bash
curl -H "Host: auth.localhost:3000" http://localhost:3000/graphql
# Returns HTML: <title>Not Found</title>
```

### Cause

The Constructive database (`constructive`) is not deployed. The GraphQL server needs to connect to a deployed database at startup to route requests correctly.

### Solution

1. Check if the database exists:
```bash
eval "$(pgpm env)"
psql -c "SELECT datname FROM pg_database WHERE datname = 'constructive';"
```

2. If the database does not exist or is empty, deploy it:
```bash
cd /path/to/constructive-db
eval "$(pgpm env)"

# Create and deploy database
dropdb --if-exists constructive
pgpm deploy --database constructive --createdb --yes --package constructive-services
pgpm deploy --database constructive --yes --package constructive-local
```

3. Restart the GraphQL server:
```bash
# Kill current process
lsof -ti:3000 | xargs kill -9

# Restart
screen -dmS graphql bash -c 'cd /path/to/constructive/graphql/server && eval "$(pgpm env)" && PGDATABASE=constructive pnpm dev'
sleep 15
```

4. Verify:
```bash
curl -s --connect-timeout 5 --max-time 10 -H "Host: auth.localhost:3000" -H "Content-Type: application/json" -X POST http://localhost:3000/graphql -d '{"query":"{ __typename }"}'
# Expected: {"data":{"__typename":"Query"}}
```

### Verification

```bash
# Check database tables
psql -d constructive -c "SELECT COUNT(*) FROM metaschema_public.database;"
# Expected: number greater than 0
```

---

## Phase 1: Agent stuck on nohup + curl verification (hang)

### Problem

After running the following commands the Agent hangs with no response:

```bash
cd .../constructive/graphql/server && PGDATABASE=constructive nohup pnpm dev > server.log 2>&1 &
sleep 3
curl -s -o /dev/null -w "%{http_code}" http://api.localhost:3000/graphql
```

### Cause

1. **curl has no timeout:** When the server is not ready, curl waits indefinitely for a connection by default.
2. **sleep 3 is too short:** GraphQL first start (compile, connect to DB) usually needs 20–30 seconds.
3. **nohup missing pgpm env:** Without `eval "$(pgpm env)"`, the DB connection may fail.
4. **api.localhost resolution:** In some environments DNS resolution is slow or broken.

### Solution (using screen)

```bash
# Run the following in the workspace root that contains the `constructive/` repo
screen -dmS graphql bash -c 'cd ./constructive/graphql/server && eval "$(pgpm env)" && PGDATABASE=constructive pnpm dev'
sleep 25

# Health check with localhost + Host header + POST + JSON
curl -s --connect-timeout 5 --max-time 10 \
  -H "Host: auth.localhost:3000" \
  -H "Content-Type: application/json" \
  -X POST \
  http://localhost:3000/graphql \
  -d '{"query":"{ __typename }"}'
```

**Key improvements:**

| Improvement | Description |
|-------------|-------------|
| `--connect-timeout 5` | Connection timeout 5s to avoid waiting forever |
| `--max-time 10` | Total request timeout 10s |
| `-H "Host: auth.localhost:3000"` + `http://localhost:3000` | Use localhost (bypass DNS) + correct Host header for routing |
| `sleep 25` | Give server enough time to start (first start usually 20–30s) |
| `eval "$(pgpm env)"` | Ensure DB connection env vars are set |

### Verification

```bash
# Check port
lsof -i:3000 | grep LISTEN

# View startup log
tail -20 ./constructive/graphql/server/server.log

# Expected: curl returns 405
```

---

## Post-Provision: Mailpit not running

### Problem

Mailpit container is not running or ports 1025/8025 are not accessible.

### Solution

```bash
# Check if container exists
docker ps -a | grep mailpit

# If not created, create and start it
docker run -d --name mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit

# If created but stopped
docker start mailpit

# Verify
curl -s http://localhost:8025 | head -5
```

---

## Post-Provision: Admin GraphQL Server (3002) hangs on startup

### Problem

After Admin server starts, port 3002 is not listening, process appears hung. Screen session exists but service is not responding.

### Cause

`constructive server` without `--origin` parameter enters interactive mode, waiting for user input for CORS origin. In Agent/CI environments without TTY input, this causes infinite waiting.

### Solution

**Must** add `--origin "*"` parameter to the command:

```bash
# ❌ Wrong - will hang waiting for input
constructive server --port 3002

# ✅ Correct - skip interactive prompt
constructive server --port 3002 --origin "*"
```

Full startup command:

```bash
screen -dmS admin-server bash -c '
  eval "$(pgpm env)" && \
  PGDATABASE=constructive \
  API_ENABLE_SERVICES=true \
  API_IS_PUBLIC=false \
  API_ANON_ROLE=administrator \
  API_ROLE_NAME=administrator \
  API_EXPOSED_SCHEMAS=metaschema_public,services_public,constructive_auth_public \
  API_META_SCHEMAS=metaschema_public,services_public,metaschema_modules_public,constructive_auth_public \
  constructive server --port 3002 --origin "*"
'
```

### Verification

```bash
# Wait for startup
sleep 10

# Check port
lsof -i:3002 | grep LISTEN

# Test GraphQL endpoint
curl -s http://localhost:3002/graphql -H "Content-Type: application/json" \
  -d '{"query":"{ __typename }"}'
# Expected return: {"data":{"__typename":"Query"}}
```

---

## Post-Provision: send-email-link not sending emails

### Problem

Emails are not appearing in Mailpit UI after user signup or password reset.

### Cause

1. send-email-link is not running
2. Admin GraphQL Server (port 3002) is not running
3. job-service is not running
4. `SEND_EMAIL_LINK_DRY_RUN=true` (should be `false`)

### Solution

**Step 1: Check all services are running:**

```bash
# Check HTTP services
for port in 3002 8082; do
  if lsof -i:$port | grep -q LISTEN; then
    echo "✅ Port $port - Running"
  else
    echo "❌ Port $port - Not running"
  fi
done

# Check job-service (no HTTP port)
if pgrep -f "knative-job-service" > /dev/null; then
  echo "✅ job-service - Running"
else
  echo "❌ job-service - Not running"
fi
```

**Step 2: Check logs for errors:**

```bash
tail -50 /tmp/send-email-link.log
tail -50 /tmp/job-service.log
tail -50 /tmp/admin-server.log
```

**Step 3: Verify `SEND_EMAIL_LINK_DRY_RUN` is false:**

If `DRY_RUN` is true, emails are logged but not sent. Restart send-email-link with `SEND_EMAIL_LINK_DRY_RUN=false`.

---

## Post-Provision: "Missing site configuration for email" error

### Problem

send-email-link logs show:

```
"Missing site configuration for email"
```

### Cause

The database has no **site domain** configured. The provision flow creates API domains (e.g., `api-xxx.localhost`, the per-DB data host) but not site domains.

### Solution

Add a site domain after provisioning:

```sql
-- Replace <your-db-name> with your database name
INSERT INTO services_public.domains (database_id, site_id, subdomain, domain)
SELECT 
    db.id,
    s.id,
    '<your-db-name>',
    'localhost'
FROM metaschema_public.database db
JOIN services_public.sites s ON s.database_id = db.id
WHERE db.name = '<your-db-name>'
ON CONFLICT (subdomain, domain) DO NOTHING;
```

### Verification

```sql
SELECT d.subdomain, d.domain
FROM services_public.domains d
JOIN services_public.sites s ON d.site_id = s.id
JOIN metaschema_public.database db ON d.database_id = db.id
WHERE db.name = '<your-db-name>';
-- Should return a row
```

---

## Post-Provision: job-service not processing jobs

### Problem

Jobs are being added to the queue but not processed. send-email-link never receives requests.

### Solution

**Step 1: Check job-service is running:**

```bash
pgrep -f "knative-job-service" || echo "Not running!"
```

**Step 2: Check logs:**

```bash
tail -f /tmp/job-service.log
# Should see: "worker-0 connected and looking for jobs..."
```

**Step 3: Verify environment variables:**

The most important variable is `INTERNAL_GATEWAY_DEVELOPMENT_MAP`:

```bash
export INTERNAL_GATEWAY_DEVELOPMENT_MAP='{"send-email-link":"http://localhost:8082"}'
```

**Step 4: Restart job-service if needed:**

```bash
pkill -f "knative-job-service"
cd $CONSTRUCTIVE_PATH/jobs/knative-job-service
# Set all env vars...
nohup node dist/run.js > /tmp/job-service.log 2>&1 &
```

---

## Phase 2.1: pgpm init non-interactive mode fails

### Problem

Running `pgpm init` even with `--no-tty` or `CI=true` still throws error:

```
Error [ERR_USE_AFTER_CLOSE]: readline was closed
```

### Cause

pgpm's inquirer dependency has issues in non-TTY environments. **Must pass all required parameters** along with `--no-tty` for it to work properly.

### Solution

**Pass all required parameters (including `--repoName`)**

For workspace, must provide all parameters (**note: `--repoName` is required**):

```bash
pgpm init workspace --no-tty \
  --name my-workspace \
  --fullName "Your Name" \
  --email "you@example.com" \
  --username your-github-username \
  --license MIT \
  --repoName my-workspace
```

For module, must provide all parameters:

```bash
pgpm init --no-tty \
  --moduleName my-module \
  --moduleDesc "My module description" \
  --fullName "Your Name" \
  --email "you@example.com" \
  --username your-github-username \
  --repoName my-workspace \
  --license MIT \
  --access public \
  --extensions "plpgsql,uuid-ossp"
```

### Verification

```bash
ls my-workspace/
# Should see: pgpm.json, pnpm-workspace.yaml, package.json, packages/
```

---

## Phase 2.2/2.3: pnpm install fails

### Problem

`pnpm install` throws error: cannot find `@constructive-io/*` packages.

### Solution

Ensure `.npmrc` is configured correctly:

```bash
cat > .npmrc << 'EOF'
@constructive-io:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
EOF
```

Ensure `GITHUB_TOKEN` environment variable is set.

---

## General: Running services in background (recommended: screen)

### Problem

After running `pnpm dev` or other server startup commands, Agent gets stuck waiting for command to complete, or process terminates after command ends.

### Recommended: screen

Using `screen` allows the server to run persistently in the background, unaffected by terminal closure:

```bash
# Start GraphQL Server (port 3000)
screen -dmS graphql bash -c 'cd /path/to/constructive/graphql/server && eval "$(pgpm env)" && PGDATABASE=constructive pnpm dev'

# Start Next.js App (port 3081)
screen -dmS app bash -c 'cd /path/to/app && pnpm dev --port 3081'

# Wait for startup
sleep 10

# Verify
lsof -i:3000 | head -3  # GraphQL
lsof -i:3081 | head -3  # Next.js
```

### screen common commands

```bash
# List all screen sessions
screen -ls

# Attach to specified session (view logs)
screen -r graphql
screen -r app

# Detach (without stopping service): Press Ctrl+A then D

# Stop specified session
screen -S graphql -X quit
screen -S app -X quit

# Stop service (by port)
lsof -ti:3000 | xargs kill  # GraphQL
lsof -ti:3081 | xargs kill  # Next.js
```

### Quick start commands

```bash
# GraphQL Server
screen -dmS graphql bash -c 'cd /path/to/constructive/graphql/server && eval "$(pgpm env)" && PGDATABASE=constructive pnpm dev'

# Next.js App
screen -dmS app bash -c 'cd /path/to/app && pnpm dev --port 3081'

# Verify startup
sleep 10
# Add timeout to prevent hang; api endpoint needs Host header
curl -s --connect-timeout 5 --max-time 10 -o /dev/null -w "%{http_code}" -H "Host: api.localhost:3000" http://127.0.0.1:3000/graphql  # Expected 405
curl -s --connect-timeout 5 --max-time 10 -o /dev/null -w "%{http_code}" http://127.0.0.1:3081  # Expected 200
```

### Option comparison

| Method | Pros | Cons |
|--------|------|------|
| **`screen`** | ✅ Persistent, interactive log viewing | Requires screen installed |
| `&` + log | ✅ Simple, no dependencies | May stop when terminal closes |
| `nohup` | ✅ Won't stop when terminal closes | Inconvenient for real-time logs |

### Verification

```bash
# Check port
lsof -i:3000 | grep LISTEN  # GraphQL
lsof -i:3081 | grep LISTEN  # Next.js

# Test API (add timeout to prevent hang)
curl -s --connect-timeout 5 --max-time 10 -o /dev/null -w "%{http_code}" -H "Host: api.localhost:3000" http://127.0.0.1:3000/graphql  # Expected 405
curl -s --connect-timeout 5 --max-time 10 -o /dev/null -w "%{http_code}" http://127.0.0.1:3081  # Expected 200
```

---

## Phase 1: Postgres connection refused

### Problem

When running `psql` or other database commands, throws error:

```
psql: error: connection to server at "localhost" (::1), port 5432 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
```

### Cause

pgpm docker needs to be run inside the `constructive-db` directory to start correctly.

### Solution

```bash
# Enter constructive-db directory
cd /path/to/constructive-db

# Start docker
eval "$(pgpm env)"
pgpm docker start

# Verify connection
psql -c "SELECT 1;"
```

### Verification

```bash
psql -c "SELECT 1;"
# Should return:
#  ?column?
# ----------
#        1
```

---

## Phase 1: role "administrator" does not exist

### Problem

When running `pgpm deploy`, throws error:

```
role "administrator" does not exist
```

### Cause

Database roles (such as `administrator`, `authenticated`, etc.) have not been created yet. Need to bootstrap these roles first.

### Solution

Before deploying the database, run bootstrap command to create necessary roles:

```bash
eval "$(pgpm env)"

# 1. Bootstrap roles
pgpm admin-users bootstrap --yes
pgpm admin-users add --test --yes

# 2. Create database
createdb constructive

# 3. Deploy database
pgpm deploy --yes --database constructive --package constructive-local
```

### Verification

```bash
psql -d constructive -c "SELECT rolname FROM pg_roles WHERE rolname = 'administrator';"
# Should return administrator role
```

---

## Phase 2.3: SDK package not found or wrong imports

### Problem

`pnpm install` throws 404 errors for SDK packages, or imports fail.

### Solution

Use the correct SDK packages from the npm registry:

| Package | Use Case |
|---------|----------|
| `@constructive-io/sdk` | **Node.js and browser** (provision, CLI, React, Next.js) |
| `@constructive-io/graphql-codegen` | SDK code generation |

See the `constructive-data-modeling` skill for setup details and the `constructive-codegen` skill for codegen.

### Verification

```bash
pnpm install
pnpm build
# No 404 or import errors
```

---

## Phase 2.3: SignUp return type missing ok/errors fields

### Problem

Compilation error:

```
Object literal may only specify known properties, and 'ok' does not exist in type 'SignUpPayloadSelect'.
```

### Cause

SDK's `signUp` mutation returns `SignUpPayloadSelect`, which has structure `{ result: { select: SignUpRecordSelect } }`, without `ok` and `errors` fields.

### Solution

**Use the correct select structure:**

```typescript
// ❌ Wrong
const signUpResult = await authDb.mutation
  .signUp(
    { input: { email, password } },
    { select: { ok: true, errors: true } }  // These fields don't exist
  )
  .execute();

// ✅ Correct
const signUpResult = await authDb.mutation
  .signUp(
    { input: { email, password } },
    { select: { result: { select: { userId: true } } } }
  )
  .execute();

// Check result
if (!signUpResult.ok || !signUpResult.data?.signUp?.result) {
  console.log('Sign up failed or user already exists');
}
```

### Verification

```bash
pnpm build
# Should compile successfully
```

---

## Phase 2.3: setHeaders method does not exist

### Problem

Compilation error:

```
Property 'setHeaders' does not exist on type '{ orgGetManagersRecord: ...; database: ...; ... }'.
```

### Cause

The ORM object returned by `createClient` does not have `setHeaders` method. This method is on the **adapter**, not the client.

The `db.setHeaders()` example in Skill documentation is idealized, actual SDK structure is different.

### Solution

**Pass headers when creating the client:**

```typescript
// ❌ Wrong
const publicDb = createPublicClient({
  endpoint: 'http://api.localhost:3000/graphql',
});
publicDb.setHeaders({ Authorization: `Bearer ${accessToken}` });  // Does not exist

// ✅ Correct
const publicDb = createPublicClient({
  endpoint: 'http://api.localhost:3000/graphql',
  headers: { Authorization: `Bearer ${accessToken}` },
});
```

### Verification

```bash
pnpm build
# Should compile successfully
```

---

## Phase 2.3: secureTableProvision/field/relationProvision not in admin SDK

### Problem

Compilation error:

```
Property 'secureTableProvision' does not exist on type ...
Property 'field' does not exist on type ...
```

### Cause

Schema operations (`secureTableProvision`, `field`, `relationProvision`, `table`) are in the **public SDK**, not the **admin SDK**. The `admin-<db>.localhost` endpoint name refers to the GraphQL **endpoint**, not the admin SDK client.

### Solution

Use the public SDK client for all schema operations. See the `constructive-data-modeling` skill for client setup and table / field usage examples.

### Verification

```bash
pnpm build
# Should compile successfully
```

---

## Phase 2.3: constructBlueprint fails with `NOT_FOUND (memberships_module)`

### Problem

`constructBlueprint` returns `status: failed` with `errorDetails: "NOT_FOUND (memberships_module)"`, and the table is **never created** (not a silent 0-row — a hard abort).

### Cause

A table declared `policies: [{ $type: 'AuthzEntityMembership', data: { entity_field: 'entity_id', membership_type: 2 }, … }]`, but the app was provisioned with the `auth:hardened` preset (or a basic auth module list), which has **no** org-scoped memberships modules. The org-scoped membership SPRT that `AuthzEntityMembership` resolves does not exist, so the construct aborts.

### Solution

Default a basic (org-less) app to **owner-scoped** policies, not entity-membership ones:

```typescript
// ✅ Each user owns their rows (default for a basic app)
nodes: ['DataId', 'DataDirectOwner', { $type: 'DataTimestamps', data: { include_id: false } }],
use_rls: true,
policies: [{ $type: 'AuthzDirectOwner', privileges: ['select','insert','update','delete'], permissive: true, data: { entity_field: 'owner_id' } }],

// ✅ App-wide shared pool (no ownership)
nodes: ['DataId', { $type: 'DataTimestamps', data: { include_id: false } }],
use_rls: true,
policies: [{ $type: 'AuthzAllowAll', privileges: ['select','insert','update','delete'], permissive: true }],

// ❌ Aborts on auth:hardened — only valid once the `b2b:storage` org modules are provisioned
policies: [{ $type: 'AuthzEntityMembership', data: { entity_field: 'entity_id', membership_type: 2 }, … }],
```

The `schemas/core.ts` template already ships the owner-scoped default. Remember the FK prereq: `owner_id` FKs to the per-tenant users table, so sign the authed user up via the TENANT endpoint (`auth-<sub>.localhost`), not base `auth.localhost`. See gotchas RLS-POLICY-001.

### Verification

```bash
# Re-run provision; the construct should report status: completed and create the table.
pnpm run provision
```

---

## Phase 3: updateUser returns 200 but does not persist (silent no-op)

### Problem

Calling `updateUser` (profile / account-settings) succeeds (HTTP 200, no error), but the username / display_name / profile_picture change is **not saved** — re-querying the user shows the old values.

### Cause

The dynamically-provisioned per-tenant `users` table has RLS enabled and a column UPDATE grant to `authenticated`, but the dynamic provisioner emits only an `auth_sel` SELECT policy and **no UPDATE policy**. RLS therefore rejects the update and 0 rows change — silently. Deterministic across tenants; `users` is module-owned, so this cannot be fixed in the blueprint.

### Solution

Apply the users-table self-update policy as a [control-plane](../constructive-architecture/SKILL.md#control-plane-vs-data-plane) step (the `provision.ts` template already runs it) — `createSecureTableProvision` on `http://modules.localhost:3000/graphql` with the provisioning sudo token:

```typescript
await modulesClient.secureTableProvision.create({
  data: {
    databaseId,                                   // tenant db uuid
    schemaId,                                     // metaschema_public.schema WHERE name='users_public'
    tableId,                                      // metaschema_public.table  WHERE name='users'
    tableName: 'users',
    useRls: true,
    policies: [{ $type: 'AuthzDirectOwner', permissive: true, privileges: ['update'],
                 policy_name: 'self_update', data: { entity_field: 'id' } }] as unknown as Record<string, unknown>,
  },
  select: { id: true },
}).unwrap();
```

This emits `auth_upd_self_update` (`FOR UPDATE TO authenticated USING id = jwt_public.current_user_id()`) and updateUser persists. See gotchas RLS-USERS-UPDATE-001. (Platform gap, flagged upstream; control-plane step is the app-side reconciliation.)

### Verification

```bash
# Expect BOTH auth_sel and auth_upd_self_update on the users table:
psql "$PGDATABASE" -c "SELECT polname FROM pg_policy WHERE polrelid =
  (SELECT oid FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
   WHERE c.relname='users' AND n.nspname LIKE '%users-public' LIMIT 1);"
# Then call updateUser and re-query the user — the change should persist.
```

---

## Phase 2.3: table.findOne parameter error (where vs id)

### Problem

Compilation error:

```
Object literal may only specify known properties, and 'where' does not exist in type '{ id: string; select: TableSelect; }'.
```

### Cause

The `findOne` method accepts the `id` parameter directly, unlike `findMany` which uses a `where` parameter.

### Solution

**Use the correct findOne parameters:**

```typescript
// ❌ Wrong - findOne has no where parameter
const result = await db.table.findOne({
  where: { id: tableId },
  select: { id: true, name: true },
}).execute();

// ✅ Correct - pass id directly
const result = await db.table.findOne({
  id: tableId,
  select: { id: true, name: true },
}).execute();
```

### findOne vs findMany parameter comparison

| Method | Parameters |
|--------|------------|
| `findOne` | `{ id: string, select: {...} }` |
| `findMany` | `{ where: {...}, select: {...}, first?: number }` |
| `findFirst` | `{ where: {...}, select: {...} }` |

### Verification

```bash
pnpm build
# Should compile successfully
```

## Phase 2.4: SDK build missing dependencies

### Problem

Building the generated SDK throws an error:

```
Cannot find module '@tanstack/react-query' or its corresponding type declarations.
Cannot find module '@constructive-io/graphql-types' or its corresponding type declarations.
Cannot find module 'graphql' or its corresponding type declarations.
```

### Cause

The generated SDK code depends on these packages, but they are not declared in `package.json`.

### Solution

**Add the missing dependencies:**

```bash
cd sdk/sdk

# Add dependencies
cat package.json | jq '
  .dependencies = {
    "@tanstack/react-query": "^5.0.0",
    "@constructive-io/graphql-types": "link:/path/to/constructive/graphql/types/dist",
    "@0no-co/graphql.web": "^1.0.0",
    "gql-ast": "^3.0.0",
    "graphql": "^16.0.0"
  }
' > package.json.tmp && mv package.json.tmp package.json

pnpm install
```

### Verification

```bash
pnpm build
# Should build successfully
```

---

## Phase 2.4: makage build missing tsconfig.esm.json

### Problem

Running `makage build` throws an error:

```
error TS5058: The specified path does not exist: 'tsconfig.esm.json'.
```

### Cause

makage requires `tsconfig.esm.json` to build ESM output.

### Solution

**Create tsconfig.esm.json:**

```bash
cat > tsconfig.esm.json << 'EOF'
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "dist/esm",
    "module": "ES2022",
    "moduleResolution": "bundler"
  }
}
EOF
```

### Verification

```bash
pnpm build
# Should build successfully, generating dist/ and dist/esm/ directories
```

---

## Phase 2.4: makage build missing README.md

### Problem

Running `makage build` throws an error:

```
ENOENT: no such file or directory, stat 'README.md'
```

### Cause

makage tries to copy README.md to the dist directory, and throws an error if the file does not exist.

### Solution

**Create README.md:**

```bash
echo "# Package Name" > README.md
```

### Verification

```bash
pnpm build
# Should build successfully
```

---

## Phase 2.3/2.4: __dirname undefined in ESM scripts

### Problem

Running TypeScript scripts in ESM mode (`"type": "module"`) throws an error:

```
ReferenceError: __dirname is not defined
```

### Cause

`__dirname` and `__filename` are CommonJS global variables that do not exist in ESM mode.

### Solution

**Use `import.meta.url` instead:**

```typescript
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Now you can use __dirname normally
const outputDir = path.resolve(__dirname, '../output');
```

### Verification

```bash
pnpm tsx script.ts
# Should run normally, no more __dirname error
```

---

## Phase 2.3: TypeScript select field does not exist (e.g. `schemaName` not in type)

### Problem

When writing provision scripts, TypeScript reports errors like:

- `Property 'schemaName' does not exist on type ...`
- `Object literal may only specify known properties ...`

### Cause

The SDK's select types are **strict**: you can only select fields that actually exist in the schema and are exposed by codegen. Many “seemingly reasonable” field names (e.g., `schemaName`) do not exist in that entity's select type.

### Solution

- **Start with minimal select** (only get `id`, `name`, and other fields you know exist), get the script working, then add fields incrementally
- **Let types be your guide**: Let your editor/TS suggestions drive field selection, rather than guessing field names
- If you just need to “confirm creation succeeded”, usually `id: true` is sufficient

### Verification

```bash
pnpm build
# TypeScript compiles successfully
```

---

## Phase 2.3: Workaround SQL schema name contains hash

### Problem

When applying workarounds (like `fix-membership-defaults`) an error is thrown:

```
ERROR:  schema "<db>-user-identifiers-public" does not exist
```

### Cause

In Per-DB mode, schema names include a hash suffix, like `<db>-a65661ed-user-identifiers-public`, not simply `<db>-user-identifiers-public` (`<db>` is your database name).

### Solution

**The `<db>-user-identifiers-public` in the documentation is a template format. Actual Per-DB schema names include a hash, so you must first run the psql query below to get the real schema name, then execute ALTER/UPDATE.**

**First query the correct schema name:**

```bash
eval "$(pgpm env)"

# Find schemas containing the database name
psql constructive -t -c "SELECT table_schema FROM information_schema.tables WHERE table_name = 'emails' AND table_schema LIKE '%<dbName>%';"

# Example output: <db>-a65661ed-user-identifiers-public
```

**Then use the correct schema name to execute SQL:**

```bash
# Replace <schema> with the actual name from the query
psql constructive -c 'ALTER TABLE "<schema>".emails ALTER COLUMN is_verified SET DEFAULT true;'
psql constructive -c 'UPDATE "<schema>".emails SET is_verified = true;'
```

**Or use a wildcard query to get it automatically:**

```bash
# Automatically find and apply
SCHEMA=$(psql constructive -t -c "SELECT table_schema FROM information_schema.tables WHERE table_name = 'emails' AND table_schema LIKE '%<db>%' LIMIT 1;" | tr -d ' ')
psql constructive -c "ALTER TABLE \"$SCHEMA\".emails ALTER COLUMN is_verified SET DEFAULT true;"
psql constructive -c "UPDATE \"$SCHEMA\".emails SET is_verified = true;"
```

### Verification

```bash
psql constructive -c "SELECT is_verified FROM \"$SCHEMA\".emails;"
# Should return true
```

---

## Phase 2.4: pgpm init fails in non-packages directory

### Problem

Running `pgpm init` in non-standard directories like `sdk/` throws an error:

```
Error: You must be inside the workspace root, a parent directory of modules (like 'packages/'), or inside one of the workspace packages
```

### Cause

`pgpm init` only recognizes `packages/` and `extensions/` as module directories. Custom directories (like `sdk/`) are not automatically recognized.

### Solution

**Option A: Manually create directories and files**

```bash
# 1. Create directory structure
mkdir -p sdk/my-package/src

# 2. Manually create package.json
cat > sdk/my-package/package.json << 'EOF'
{
  "name": "@myapp/my-package",
  "version": "0.0.1",
  "main": "index.js",
  "module": "esm/index.js",
  "types": "index.d.ts",
  "publishConfig": {
    "access": "public",
    "directory": "dist"
  },
  "scripts": {
    "clean": "makage clean",
    "build": "makage build"
  },
  "devDependencies": {
    "makage": "^0.1.10"
  }
}
EOF

# 3. Create tsconfig.json and tsconfig.esm.json
# 4. Create README.md
# 5. Update pnpm-workspace.yaml to add 'sdk/*'
```

**Option B: Create in packages/ first, then move**

```bash
# 1. Create in packages/
cd /path/to/workspace
pgpm init -t pnpm/module --no-tty --moduleName my-package ...

# 2. Move to target directory
mkdir -p sdk
mv packages/my-package sdk/

# 3. Update pnpm-workspace.yaml
```

### Verification

```bash
pnpm install
pnpm build
# Should install and build successfully
```

---

## Phase 2.5: `pgpm init -w` / template init hangs (waiting for input)

### Problem

When running Next.js template initialization, the command hangs (especially noticeable in non-interactive environments), appearing to wait indefinitely for input parameters (e.g., moduleName).

### Cause

`pgpm init` in non-TTY / agent environments may enter interactive prompts and cause “hung waiting for input” if required parameters are missing.

### Solution

- **Prefer using `--no-tty` and explicitly providing required parameters** (see the `pgpm init` non-interactive mode section under Phase 2.1 in this file)
- Or ensure you're running in a truly interactive terminal

### Verification

The initialization command finishes within a reasonable time and generates the app directory and `package.json`.

---

## Phase 2.5: Next.js 500 after start (missing `@sdk/*` / generated output)

### Problem

After starting `pnpm dev`, pages return 500 errors. Common log messages include:

- `Cannot find module '@sdk/auth'` (or similar `@sdk/*`)
- Cannot find the generated GraphQL SDK output directory/files

### Cause

This template typically depends on codegen-generated `@sdk/*` artifacts (located in the app's `src/graphql/...` or similar directory). If you run `pnpm dev` without first running `pnpm codegen`, the missing modules will cause 500 errors.

### Solution

Run in the app directory:

```bash
pnpm install
pnpm codegen
pnpm dev
```

### Verification

After `pnpm dev`, pages load normally; and `@sdk/*` imports no longer throw errors.

---

## Phase 3: Auth hook onSuccess cannot get password (Per-DB login needs mutationFn to return password)

### Problem

When calling `appSignIn(email, password)` in the boilerplate's login hook `onSuccess`, the `password` is not available (undefined or inaccessible), causing Per-DB login to fail.

### Cause

The `onSuccess` callback only receives the **mutation's return value**. If mutationFn only returns `{ token, email, rememberMe }` without `password`, you cannot access the user's just-entered password in `onSuccess` (it wasn't passed down via closure or parameters).

### Solution

Make **mutationFn's return value include `password`**, so `onSuccess` can destructure it:

- For example: `return { token, email, password, rememberMe }`
- In `onSuccess`: `const { token, email, password } = data; await appSignIn(email, password);`

Do not rely on “getting password from somewhere else in onSuccess” - if the mutation didn't return it, don't assume you can get it.

### Verification

After login, Per-DB's `appSignIn` executes correctly, and the app token is present in localStorage.

---

## Phase 3: configure-app-sdk `headers` type error (pass object, not function)

### Problem

Writing this in `src/lib/configure-app-sdk.ts`:

```ts
configure({
  endpoint: APP_ENDPOINT,
  headers: () => {
    const token = getAppToken();
    return token ? { Authorization: `Bearer ${token}` } : {};
  },
});
```

Build or runtime error: type mismatch, or SDK requests at runtime don't include Authorization.

### Cause

The generated SDK's `createClient` / `configure` expects **`headers: Record<string, string>`** (a plain object), not a function. Passing a function causes type errors or unexpected behavior.

### Solution

Compute the headers object first, then pass it in:

```ts
function getHeaders(): Record<string, string> {
  const token = getAppToken();
  return token ? { Authorization: `Bearer ${token}` } : {};
}

configure({
  endpoint: APP_ENDPOINT,
  headers: getHeaders(),
});
```

If the token changes after login, you need to call `configure({ endpoint, headers: getHeaders() })` again after successful login, or use an adapter that provides headers on each request.

### Verification

SDK requests correctly include `Authorization: Bearer <token>`, with no type errors.

---

## Phase 3: Route structure (don't assume route groups; check template app directory first)

### Problem

When creating new features, you placed pages under an assumed route group, for example:

- `src/app/(authenticated)/my-feature/page.tsx`

But you discover that other pages in the template are not under `(authenticated)`, or the template doesn't use that route group at all, causing routing anomalies or inconsistent page styles.

### Cause

Different Next.js templates organize routes differently. Some templates use route groups like `(authenticated)`, while others place pages directly under `app/` (e.g., `app/users/`, `app/account/`). **Do not assume based on experience** - you must first examine the current template's actual structure.

### Solution

1. **First examine the template's existing structure**: Check what directories are under `src/app/` and whether there are `(xxx)` route groups.
2. **Cross-reference `src/app-routes.ts`**: The route registry reflects the template's conventional structure (e.g., paths for `users`, `account`, etc.).
3. **Keep new features consistent with existing pages**: If the template uses a flat structure like `app/users/`, `app/account/`, new features should also go under `app/<feature>/` - don't create route groups that don't appear in the template.

### Verification

New page paths are consistent with other template pages, accessible normally through `app-routes.ts`, with no 404 or layout issues.

---

## Phase 3: Don't wrap page in AppShell again (layout already has AuthenticatedShell)

### Problem

Wrapping page content in `<AppShell>`, for example:

```tsx
export default function BoardsPage() {
  return (
    <AppShell navigation={...} topBar={...}>
      ...
    </AppShell>
  );
}
```

Runtime error about missing `navigation`, `topBar` props, or duplicate/broken layout.

### Cause

The template's **layout already wraps all child pages in `AuthenticatedShell`** (or an equivalent component), providing a unified shell (navigation, top bar, etc.). **Pages themselves do not need another AppShell wrapper**. SKILL.md may not explicitly state this, making it easy to assume “one shell per page”.


…(truncated)
