Developer docs
Data Model·

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>
TransformReason
@ is strippedNot valid in a table name
- → _PostgreSQL convention
/ → _Separates namespace and plugin name (single underscore)
plg_ prefixAvoids 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:

PropertyRequiredDescription
resourceRequiredThe 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)
sharedOptional"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_id
  • created_at / updated_at
  • deleted_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):

TypeUse
TEXTStrings, dates (ISO), enums
INTEGERIntegers, amounts (manage yen as integers)
REALDecimals

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 shared table through the offline path (niyase.data) instead of niyase.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.