Onumia

Database Isolation

View as Markdown

Onumia gives each sandbox its own database state. It does this by cloning your WordPress tables under a sandbox-specific table prefix and routing sandbox requests to that prefix, so an agent can prepare and preview database changes without ever writing to your live tables.

The Managed Drop-In

The mechanism that makes this work is a WordPress database drop-in. WordPress loads a file named db.php from the content directory before almost anything else, which makes it the ideal place to decide, per request, which set of tables WordPress should talk to. On activation, Onumia installs a managed db.php into wp-content. When a request belongs to an active sandbox, it points WordPress at that sandbox’s tables; when it does not, the request runs against the live database exactly as before, so ordinary WordPress code never has to know which one it is using.

Drop-In Conflicts

Onumia will never overwrite a db.php it does not recognize as its own. If another plugin or your host already ships a custom database drop-in, Onumia refuses to install over it and reports the conflict. You must resolve that conflict, by reconciling or replacing the existing drop-in, before sandbox database routing can work, because two drop-ins cannot both own db.php.

Cloning Into A Sandbox

Creating a sandbox locally means building a parallel set of tables under the sandbox prefix. Onumia does this carefully, because a naive full copy of a large production database is both slow and wasteful for what is meant to be a working preview.

Before copying anything, Onumia estimates how much row data the clone would actually move. If that estimate exceeds the local clone limit, creation is blocked before any side effects occur. If Onumia cannot estimate the size at all, it also blocks by default rather than risk an unbounded copy. Only once the size check passes does it create the sandbox tables, copy each source table’s schema, copy row data for tables that are not schema-only, and redact configured columns as it goes. Tables that hold Onumia’s own control data are excluded from clone sources entirely.

The Local Clone Limit

The default ceiling on copied row data is 250 MB. The limit is measured against the row data Onumia will actually copy, not against schema-only tables whose rows are skipped on purpose. You can change it from the Onumia settings screen or in code:

add_filter('onumia/database/local_clone_max_bytes', fn (int $bytes): int => 500 * 1024 * 1024);

If your host prevents Onumia from inspecting table sizes and you have separately confirmed that cloning is acceptable, you can opt into unknown-size clones, though this is off by default for a reason:

add_filter('onumia/database/allow_unknown_size_local_clone', '__return_true');

Schema-Only Tables

Some tables should exist in the sandbox but start empty. Log, telemetry, session, and cache tables are the obvious cases: their schema matters for compatibility, but copying millions of historical log rows into a preview clone is pure overhead. By default, tables whose names match log-like patterns are cloned schema-only, so the table exists in the sandbox but contains no rows.

You control this with two filters. Pattern matching covers families of tables, while the explicit list pins a single known table:

add_filter('onumia/database/schema_only_table_patterns', function (array $patterns): array {
    $patterns[] = '/_events$/';
    return $patterns;
});

add_filter('onumia/database/schema_only_tables', function (array $tables): array {
    $tables[] = 'wp_my_plugin_audit';
    return $tables;
});

Redacted Columns

Redaction sits between a full copy and a schema-only skip: the row is copied, but a chosen column’s content is not. This keeps a table’s rows intact for previewing while leaving sensitive or noisy column content out of the clone. Onumia matches column names case-insensitively and, when it redacts one, writes a safe empty, null, default, or zero value chosen to fit the column’s shape.

The defaults target common log and trace columns: log, logs, log_data, log_message, debug_log, stack_trace, and trace. Extend the list for your own schema:

add_filter('onumia/database/redacted_columns', function (array $columns): array {
    $columns[] = 'api_secret';
    return $columns;
});

Guarding Sandbox Writes

Routing alone is not the whole safety story. While a sandbox is active, Onumia also guards mutating queries so that sandbox work cannot accidentally escape into live data. The guard blocks writes aimed at live-prefixed tables, writes to unscoped tables, and ambiguous mutating statements it cannot confidently attribute to the sandbox. A mutation is allowed only when it targets the selected sandbox prefix, or when Onumia itself is performing an internal control-plane operation.

Onumia also records the database mutations a sandbox makes, capturing the statement type and the target tables for each one. These records are not the isolation mechanism; they exist to support the later review and database merge workflows by giving you a trail of what changed.

What Isolation Does Not Cover

Database isolation is exactly that: isolation of your local WordPress tables. It does not make external systems sandbox-aware. If code running in a sandbox calls a third-party API, writes to an external database, enqueues a job, or sends mail, those side effects happen for real and are outside the table prefix boundary. Treat any external integration in agent code as something to review on its own terms, because Onumia cannot roll back an HTTP request the way it can a sandbox table.