Step 1: local Postgres to lab_* staging.
Status: Operational — use as Step 1 of PUBLISH_PIPELINE.md Audience: Admins, developers Last updated: August 2026
Related:
/data-management/publish-to-cloud/data-generation/db-mappernpm run audit:cloudLab staging copies local PostgreSQL authoring data into Supabase tables prefixed with lab_. These tables are:
verbs, sentences, … used by Tabula / public apps)admin and super_admin onlyWithout publishing to lab_*, the deployed Lab shows empty overview, dictionary, and verb pages even though your machine has thousands of rows locally.
After publish, the deployed app reads and writes lab_* directly when:
CORPUS_DATA_SOURCE is unset or auto (production defaults to Supabase lab), andadmin / super_admin), or the server has SUPABASE_SERVICE_ROLE_KEY.| Action | Local dev | Deployed Lab | |--------|-----------|--------------| | View overview, dictionary, verbs | Local Postgres | lab_* | | Create / update / delete rows | Local Postgres | lab_* | | CSV import (dictionary, sentences, longer texts) | Local Postgres | lab_* | | Migrate dictionary → verbs | Local Postgres | lab_* |
Two valid workflows:
lab_*.lab_*.If writes fail with auth errors, set SUPABASE_SERVICE_ROLE_KEY on Vercel Production or confirm admin login. Check /api/corpus-source (admin only) for corpusClientMode.
| | Lab staging (lab_*) | Production (live) | |---|---------------------------|------------------------| | Examples | lab_verbs, lab_sentences | verbs, sentences | | Used by | Deployed Lab, admin review | Tabula, learner apps | | Kifuliiru required? | Optional on local dev; enforced on deployed Lab | Always | | Risk | Low — admin-only staging | High — affects live users | | Current row count | 0 (Aug 2026 — never published) | Partial (e.g. verbs 321, sentences 940) |
Rule: Always publish to lab_* first, validate on deployed Lab, then publish Kifuliiru-ready rows to production when approved.
Local kifuliiru database populated (authoring machine).
DB_HOST=localhost
DB_PORT=5432
DB_USER=...
DB_PASSWORD=...
DB_NAME=kifuliiru
```text
### 2. Supabase credentials
```bash
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your_anon_key
```text
Same project as Tabula Kifuliiru auth.
### 3. Lab tables exist in Supabase
Migration: `supabase/migrations/20250623000000_lab_tables.sql`
Apply via Supabase SQL editor or:
```bash
npm run db:generate-lab-migration # if regenerating from local schema
# then apply in Supabase dashboard
```text
Creates `lab_verbs`, `lab_dictionary_nouns`, `lab_sentences`, … with RLS policies using `is_lab_admin()`.
### 4. Admin role
Your Supabase user must have `admin` or `super_admin` in `user_roles` (same as Lab login).
---
## Table mapping
Local names are unchanged. Staging adds the `lab_` prefix.
| Local table | Supabase staging | Production (when ready) | |-------------|------------------|-------------------------| | `verbs` | `lab_verbs` | `verbs` | | `dictionary_nouns` | `lab_dictionary_nouns` | `dictionary_nouns` | | `dictionary_adjectives` | `lab_dictionary_adjectives` | `dictionary_adjectives` | | `dictionary_adverbs` | `lab_dictionary_adverbs` | `dictionary_adverbs` | | `sentences` | `lab_sentences` | `sentences` | | `generated_sentences` | `lab_generated_sentences` | `generated_sentences` | | `longer_texts` | `lab_longer_texts` | `longer_texts` | | `semantics_dictionary` | `lab_semantics_dictionary` | `semantics_dictionary` | | `conjugation_rules` | `lab_conjugation_rules` | `conjugation_rules` | | … | `lab_{name}` | `{name}` |
Full list: `getAllMappableTables()` in `src/lib/utils/table-mapping.ts`.
**Not published:** `wemp_users` (legacy local auth). Cloud auth uses Supabase `auth.users`.
---
## Step-by-step: first lab publish
### Step 1 — Audit gap
```bash
npm run audit:cloud
```text
Confirm `lab_*` columns show **0** and local columns show your real counts.
### Step 2 — Open publish UI
Navigate to **`/data-management/publish-to-cloud`**.
### Step 3 — Test connection
Click **Test connection** — must reach Supabase with your env credentials.
### Step 4 — Select destination
Choose **Lab staging (`lab_*`)** — not Production.
### Step 5 — Kifuliiru filter (local dev)
| Setting | First batch recommendation | |---------|----------------------------| | **Require Kifuliiru-side text** | **Off** for initial smoke test (publish all rows); **On** to mirror deployed Lab behavior |
Production destination **always** requires Kifuliiru — filter is locked.
Env override: `NEXT_PUBLIC_REQUIRE_KIFULIIRU_FOR_PUBLISH=true|false`
### Step 6 — Scan all tables
Click **Scan all tables**. Review per table:
| Metric | Meaning | |--------|---------| | Local count | Rows in your Postgres | | Cloud count | Rows already in `lab_*` | | Pending / new in local | Rows eligible to insert | | Publish-ready pending | Subset with Kifuliiru text |
### Step 7 — Select tables and publish
**Recommended first batch (high value, validates deploy):**
1. `verbs` → `lab_verbs`
2. `sentences` → `lab_sentences`
3. `dictionary_nouns` → `lab_dictionary_nouns`
4. `conjugation_rules` → `lab_conjugation_rules`
Optional second batch:
- `generated_sentences`, `longer_texts`, dictionary adjectives/adverbs, `concrete_questions`
Select tables → **Publish selected** → confirm.
### Step 8 — Verify
```bash
npm run audit:cloud
```text
`lab_*` counts should match pending rows inserted.
Re-open publish UI → **Scan** — status should move toward **In sync** for published tables.
### Step 9 — Deployed Lab smoke test
On deployed Lab (after app reads `lab_*` — see [IMPACT_VARIABLES.md](IMPACT_VARIABLES.md) Phase 3):
- `/overview` — should show non-zero stats
- `/verbs` — should list published verbs
- Dictionary pages — should show published nouns
---
## API (programmatic publish)
Same mapper engine as the UI. Always set `"target": "lab"`.
### Compare one table
```http
GET /api/mapper/compare?table=verbs&target=lab
```text
### Compare all tables
```http
GET /api/mapper/compare?target=lab
GET /api/mapper/compare?target=lab&publishReady=true
```text
### Sync one table to lab
```http
POST /api/mapper/sync
Content-Type: application/json
{
"table": "verbs",
"direction": "to-supabase",
"target": "lab",
"publishReady": false
}
```text
### Batch sync
```http
POST /api/mapper/sync-batch
Content-Type: application/json
{
"tables": ["verbs", "sentences", "dictionary_nouns"],
"target": "lab",
"publishReady": false
}
```text
**Insert-only:** Rows already in `lab_*` (matched by dedup key) are skipped — not overwritten.
---
## Dedup and matching
Rows are considered “already in cloud” when the **dedup key** matches:
1. **`english`** if non-empty
2. Else first non-empty Kifuliiru-side field: `verb_kifuliiru`, `kifuliiru`, `igambo`, `question_kifuliiru`, `conjugated_form`, …
Field-level diffs are shown in compare UI but **not auto-synced** (updates require manual edit or future sync-v2).
Implementation: `src/lib/utils/publish-readiness.ts`, `src/lib/services/db-mapper.ts`.
---
## Kifuliiru publish readiness
A row is **publish-ready** when any Kifuliiru-side field is non-empty:
`verb_kifuliiru`, `kifuliiru`, `igambo`, `igambo_plural`, `question_kifuliiru`, `conjugated_form`
| Context | Filter | |---------|--------| | Local dev → `lab_*` | Off by default (publish everything) | | Deployed Lab policy | On (matches production discipline) | | Local dev → **production** | **Always on** |
---
## What publish does NOT do
| Not included | Notes | |--------------|-------| | UPDATE existing cloud rows | Insert new rows only | | Publish to `impact_variables` | Separate metrics sync — [IMPACT_VARIABLES.md](IMPACT_VARIABLES.md) | | Create missing Supabase tables | Run migration first | | Sync `semantic_relationships` | Not in `lab_*` migration yet — see [SEMANTICS_CURRENT.md](SEMANTICS_CURRENT.md) | | Pull from cloud by default | Use `direction: "from-supabase"` explicitly |
---
## Troubleshooting
| Symptom | Cause | Fix | |---------|-------|-----| | All `lab_*` counts 0 after publish | Publish never run or wrong target | Re-run with `target=lab` | | Insert permission denied | RLS / not admin | Verify `user_roles` | | 0 pending but local has rows | All dedup keys already in cloud | Check compare details | | Rows skipped | `publishReady=true` without Kifuliiru | Turn filter off or add Kifuliiru | | Table missing in scan | Not in Supabase | Apply `20250623000000_lab_tables.sql` | | Deployed Lab still empty | App reads local PG, not Supabase | Wire `/api/stats` to `lab_*` (planned) |
---
## Recommended publish phases
### Phase A — Smoke test (admin)
- [ ] `verbs`, `sentences` → `lab_*`
- [ ] Run `npm run audit:cloud`
- [ ] Confirm deployed Lab can read staging data (when wired)
### Phase B — Dictionary core
- [ ] All `dictionary_*` split tables → `lab_*`
- [ ] `generated_sentences`, `longer_texts`
### Phase C — Templates & questions
- [ ] `conversation_templates`, `concrete_questions`, template vocabulary
- [ ] `semantics_dictionary`
### Phase D — Promote to production (Step 2)
- [ ] **Scan promotion** on publish page
- [ ] Promote Kifuliiru-ready rows from `lab_*` → production
- [ ] See [PUBLISH_PIPELINE.md](/tabula-kifuliiru/guides/publish-pipeline)
### Phase E — Impact metrics
- [ ] After production/lab counts stable → sync `impact_variables` corpus slugs
- [ ] See [IMPACT_VARIABLES.md](IMPACT_VARIABLES.md)
---
## Security
- Never commit `.env` with service role key to git.
- `NEXT_PUBLIC_*` = browser-visible — anon key only.
- **`lab_*`** is admin-only RLS — safe for draft data.
- **Production publish** affects Tabula live data — double-check destination toggle.
---
## Code references
| File | Role | |------|------| | `src/app/data-management/publish-to-cloud/page.tsx` | Publish UI | | `src/app/data-generation/db-mapper/page.tsx` | Compare UI | | `src/lib/services/db-mapper.ts` | Compare & sync engine | | `src/lib/utils/table-mapping.ts` | `local` → `lab_*` names | | `src/lib/utils/publish-target.ts` | `lab` vs `production` | | `src/lib/utils/publish-policy.ts` | Kifuliiru filter by environment | | `src/app/api/mapper/compare/route.ts` | Compare API | | `src/app/api/mapper/sync/route.ts` | Single-table sync | | `supabase/migrations/20250623000000_lab_tables.sql` | Staging schema + RLS |
---
## Related documentation
| Doc | Focus | |-----|--------| | [PUBLISH_TO_CLOUD.md](/tabula-kifuliiru/guides/publish-to-cloud) | Lab + production, API summary | | [IMPACT_VARIABLES.md](IMPACT_VARIABLES.md) | KPI sync after publish | | [DATABASE_MAPPER.md](DATABASE_MAPPER.md) | Bidirectional mapper UI | | [TABLE_MAPPING_CONFIG.md](TABLE_MAPPING_CONFIG.md) | Mapping config details |