Data Model
The plg_<scope>_<name>_* table-naming convention, auto-added columns, and how sync and deactivation affect plugin data.
As a rule, plugins should be self-contained within their own tables. For reading core data (employees, departments, etc.), see Core API Integration (direct DB access is not allowed).
CRUD with niyase.data
Operate on the tables declared in the manifest's tables[] through the bridge.
const niyase = useNiyase();
await niyase.data.list("task", { limit: 50 }); // → { items, nextCursor? }
await niyase.data.get("task", id);
await niyase.data.create("task", { title: "現場A" });
await niyase.data.update("task", id, { status: "DONE" });
await niyase.data.remove("task", id); // soft delete
Internally this calls /nplg/:pluginId/:table (authorized by the user session + the current space).
Undeclared table names or columns are rejected with a 400.
Physical table-name convention
Each environment's loader expands the manifest's logical table name into the following. You never need to compute this by hand — it's here so you can recognize the real name when inspecting the bridge, sync traffic, or a DB browser.
plg_<scope>_<name>_<table>
| Transform | Reason |
|---|---|
@ is stripped | Not valid in a table name |
- → _ | PostgreSQL convention |
/ → _ | Separates namespace and plugin name (single underscore) |
plg_ prefix | Avoids collisions with core tables |
Example: task of @your-org/tasks → plg_your_org_tasks_task.
Keep plugin and table names short enough to stay within Postgres's 63-character identifier limit.
This rule is shared by cloud (Postgres), desktop (PGlite), mobile (SQLite), and the DevKit's MOCK bridge.
Additional table-definition properties
Each entry in tables[] can declare more than just name and columns:
| Property | Required | Description |
|---|---|---|
resource | Required | The permission-resource name. It becomes part of the permission string plg:<pluginId>:<resource>:<action>, and the table's RLS policy is generated from it. A table you don't declare a resource for cannot be created (fail-closed) |
shared | Optional | "connection-read" / "connection-readwrite". Only declare this on tables you share with a CLIENT in a PROVIDER-CLIENT pair (valid only on a manifest with pairedPluginId set and defaultRole: "PROVIDER") |
Auto-added columns
The loader adds the following automatically, so you do not need to write them in columns. This base column contract is identical across cloud, desktop, mobile, and the MOCK bridge.
id(primary key)workspace_idcreated_at/updated_atdeleted_at(soft delete)
Seeding master data
Tables are created empty every time they're activated — there's no automatic seeding of default master data.
If your plugin needs master data, bundle it as a csv/json asset with your plugin, and let the user explicitly import it from the UI (e.g. a "Import master data" button on a "Settings" tab), via niyase.data.create or a bulk-create call. Users can then edit it freely.
The older mechanism — auto-seeding via per-industry presets (
industryPresets[].seedData) — has been retired. Don't use it in new plugins.
Column types
Declare with three platform-independent types (the loader translates them to each DB's physical type):
| Type | Use |
|---|---|
TEXT | Strings, dates (ISO), enums |
INTEGER | Integers, amounts (manage yen as integers) |
REAL | Decimals |
Sync and the table lifecycle
plg_* tables ride the same sync stream as any other space data, but a few behaviors are specific to plugins. Knowing these will help when you hit activation, deactivation, or cross-device errors during development.
Deactivation / uninstall means the table gets dropped (this is expected)
When a plugin is deactivated, the server immediately removes that table's RLS policies, masked view, and write guards, cutting off access right away. After that, desktop and mobile physically DROP the plugin's local tables — the data itself isn't deleted server-side (it stays, access-gated), but each device's local copy is wiped entirely.
This isn't a bug — it's a deliberate design called the "revocation contract." Design your plugin assuming that when a user deactivates or uninstalls it, its local tables disappear. Re-activating brings the tables back on the next sync cycle, and whatever data survived on the server is restored via a full initial fetch.
Schema changes propagate automatically through the "handshake"
Devices periodically compare a schema version with the server (a stateless hash). When you ship a plugin update that adds a table or column to the manifest, that hash changes, and devices automatically fetch the new tables/columns on their next sync. There's no manual migration step. A newly appeared table's first fetch is always a full fetch, never an incremental one.
PROVIDER-CLIENT shared tables aren't visible offline
Tables declared with tables[].shared are deliberately excluded from what gets synced to devices. The CLIENT side can only reach them online, through /nplg-conn/:connectionId/:table. Shared data being invisible offline is a known constraint, not a missing feature.
If you hit a sync error: a 4xx usually means a table-declaration mismatch
Requests for an unknown table used to fail with a 500. Now they're rejected clearly with 409 Conflict (UNKNOWN_TABLE). If you see this while developing, check for:
- A table name that doesn't match the name derived from your plugin ID (i.e. you hand-wrote a physical name instead of letting it be derived)
- A table you forgot to declare in
manifest.tables[] - Trying to read a
sharedtable through the offline path (niyase.data) instead ofniyase.connections.data(connectionId)
UNKNOWN_TABLE is only returned once the server has confirmed the table doesn't exist in its authoritative schema, so it's distinguishable from a transient network error. When desktop receives it, it cleans up the orphaned local table using the same procedure as deactivation, and converges automatically.