Promote reviewed data toward production consumers.
Last updated: August 2026
Push new local PostgreSQL rows to the shared Supabase project (same project as Tabula Kifuliiru). Inserts only — existing cloud rows are never overwritten.
UI: /data-management/publish-to-cloud
Lab staging runbook: PUBLISH_TO_LAB.md step-by-step for lab_*
Impact metrics after publish: IMPACT_VARIABLES.md
Related: DATABASE_MAPPER.md, TABLE_MAPPING_CONFIG.md, PROJECT_OVERVIEW.md
| Destination | Supabase tables | Used by | Kifuliiru required | |-------------|-----------------|---------|-------------------| | Lab staging | lab_* (e.g. lab_verbs) | Deployed Lab, admin review | Optional locally; enforced on deployed Lab | | Production (live) | Unprefixed (e.g. verbs) | Tabula, public apps | Always |
Choose the destination on the publish page before scanning or publishing.
Note (Aug 2026): All
lab_*staging tables are currently empty. Local lab has thousands of rows; publish tolab_*before expecting deployed Lab pages to show data. Runnpm run audit:cloudto compare.
kifuliiru database populated..env or .env.local:SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your_supabase_anon_key
DB_HOST=localhost
DB_USER=...
DB_PASSWORD=...
DB_NAME=kifuliiru
```text
3. Admin role in Supabase (`admin` or `super_admin` via `user_roles`) if RLS is enabled on target tables.
---
## Workflow (UI)
1. Open **`/data-management/publish-to-cloud`**
2. **Test connection**
3. Select **Lab staging (lab_*)** or **Production (live)**
4. Optionally toggle **Require Kifuliiru-side text** (locked on for Production)
5. **Scan all tables** — compares local vs the selected Supabase destination
6. Select tables with pending rows → **Publish selected**
### What gets published
- Only rows **not already** in the target table (matched by dedup key).
- Dedup key: `english` when set; otherwise first non-empty Kifuliiru-side field (`kifuliiru`, `verb_kifuliiru`, `igambo`, etc.).
- Field-level differences are shown for review but **not** auto-synced.
---
## Kifuliiru publish readiness
A row is eligible when **any** of these fields is non-empty:
`verb_kifuliiru`, `kifuliiru`, `igambo`, `igambo_plural`, `question_kifuliiru`, `conjugated_form`
| Target | Filter behavior | |--------|-----------------| | **Production** | Always on — English-only drafts are skipped | | **Lab staging (local dev)** | Off by default — publish everything; turn on to preview deployed behavior | | **Lab staging (deployed)** | On — same as `NODE_ENV=production` policy |
Override for testing: `NEXT_PUBLIC_REQUIRE_KIFULIIRU_FOR_PUBLISH=true|false` in `.env.local`.
Implementation:
| File | Role | |------|------| | `src/lib/utils/publish-target.ts` | Destination (`lab` \| `production`), table name resolution | | `src/lib/utils/publish-policy.ts` | Lab deploy vs local filter policy | | `src/lib/utils/publish-readiness.ts` | Row-level Kifuliiru field checks, dedup keys | | `src/lib/services/db-mapper.ts` | Compare & sync engine | | `src/lib/utils/table-mapping.ts` | `local` → `lab_*` mapping for staging |
---
## Table name resolution
Local table names stay unchanged in PostgreSQL. Supabase target depends on destination:
| Local | Lab staging | Production | |-------|-------------|------------| | `verbs` | `lab_verbs` | `verbs` | | `dictionary_nouns` | `lab_dictionary_nouns` | `dictionary_nouns` | | `semantics_dictionary` | `lab_semantics_dictionary` | `semantics_dictionary` | | `sentences` | `lab_sentences` | `sentences` |
Full list: `getAllMappableTables()` in `src/lib/utils/table-mapping.ts`.
---
## API (mapper routes)
### Compare
```http
GET /api/mapper/compare?target=lab
GET /api/mapper/compare?target=production&publishReady=1
GET /api/mapper/compare?table=verbs&target=production
```text
Query parameters:
| Param | Values | Description | |-------|--------|-------------| | `target` or `publishTarget` | `lab`, `production`, `prod` | Destination (default: `lab`) | | `publishReady` | `1`, `true` | Count/sync only Kifuliiru-ready rows | | `table` | table name | Compare one table; omit for all |
### Sync (single table)
```http
POST /api/mapper/sync
Content-Type: application/json
{
"table": "verbs",
"direction": "to-supabase",
"target": "production",
"publishReady": true
}
```text
### Sync batch
```http
POST /api/mapper/sync-batch
Content-Type: application/json
{
"tables": ["verbs", "dictionary_nouns"],
"target": "lab",
"publishReady": false
}
```text
`direction: "from-supabase"` pulls new cloud rows into local Postgres (uses the same `target` for table resolution).
---
## Mappable tables (summary)
Dictionary, content, conversations, questions, templates, semantics, number–noun agreement, conjugation rules — see [PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md#database-tables-mappable-to-cloud).
**Not published:** `wemp_users` (local NextAuth only). Cloud auth uses Tabula Supabase `auth.users` + `user_roles`.
---
## Troubleshooting
| Symptom | Likely cause | |---------|----------------| | Connection failed | Wrong `SUPABASE_URL` / `SUPABASE_ANON_KEY` | | 0 cloud rows on scan | Table missing in Supabase or wrong `target` | | Insert permission denied | RLS on production tables — need admin role or policy update | | Rows skipped | No Kifuliiru-side text (Production) or already in cloud (dedup key match) | | Counts stale after filter change | Re-run **Scan all tables** |
---
## Security notes
- Never commit `.env` with real keys.
- `NEXT_PUBLIC_*` vars are browser-visible — use anon key only, not service role.
- Production publish affects live Tabula data — confirm destination before publishing.
---
## Related documentation
| Doc | Focus | |-----|--------| | [PUBLISH_TO_LAB.md](/tabula-kifuliiru/guides/publish-to-lab) | **Lab staging** — first publish, phases, troubleshooting | | [IMPACT_VARIABLES.md](IMPACT_VARIABLES.md) | Impact KPIs fed from published corpus | | [PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md) | Architecture, `lab_*` migration, auth | | [DATABASE_MAPPER.md](DATABASE_MAPPER.md) | Bidirectional compare/sync UI | | [TABLE_MAPPING_CONFIG.md](TABLE_MAPPING_CONFIG.md) | `lab_*` prefix mapping | | [SEMANTICS_CURRENT.md](SEMANTICS_CURRENT.md) | Semantics cloud gaps (`semantic_relationships`) |