| name | magicbase |
| description | Use when an HTML micro-app needs MagicBase persistence, window.Magic.db row operations, current-user context through window.Magic.getContext, creators, owners, assignees, permissions, CRUD, forms, surveys, todos, dashboards, filters, statistics, exports, or any saved user data. |
MagicBase for HTML Micro-apps
Use this skill when an HTML micro-app needs persistent data, current-user identity, ownership, permissions, or runtime row-level database operations.
MagicBase has three operation surfaces:
- Schema work is done by agent tools before HTML generation:
query_magicbase_tables, get_magicbase_table, create_magicbase_table, create_magicbase_column, update_magicbase_table_permissions, delete_magicbase_table, update_magicbase_column, and delete_magicbase_column.
- Explicit agent-side data maintenance uses
query_magicbase_rows, create_magicbase_row, batch_create_magicbase_rows, delete_magicbase_row, and batch_delete_magicbase_rows.
- Runtime row operations are done inside HTML with
window.Magic.db, using real table IDs returned by MagicBase tools.
Do not expose schema creation inside HTML pages. HTML code should only read and write rows on tables that already exist.
Project memory uses MICRO-APP.md for the latest human-readable app memory and MagicBase data model. Do not edit it directly with file-editing tools. MagicBase schema tools maintain .magicbase/migrations.json; after successful table or column changes, they refresh the latest MagicBase data model in MICRO-APP.md. If tools report a Pending migration at the start of a later task, query MagicBase first so confirmable records can be repaired before more schema work. If a legacy HTML-APP.md exists, read it for context and migrate the memory to MICRO-APP.md before finishing.
MagicBase exposes a simplified MySQL-like column model. Use only these data_type values when creating tables or columns: text, number, datetime, boolean, json.
Datetime Values
For datetime columns, MagicBase accepts only these input formats:
YYYY-MM-DD, normalized to YYYY-MM-DD 00:00:00.
YYYY-MM-DD HH:mm.
YYYY-MM-DD HH:mm:ss.
YYYY-MM-DDTHH:mm.
YYYY-MM-DDTHH:mm:ss.
- ISO 8601 values with a UTC or numeric timezone, with optional fractional seconds, such as
2026-08-04T03:12:18.582Z or 2026-08-04T11:12:18.582+08:00.
Use the raw value from <input type="date"> or <input type="datetime-local"> when it matches one of these formats. Date.prototype.toISOString() output is also accepted. MagicBase converts timezone-aware values to the service timezone and normalizes all accepted values to YYYY-MM-DD HH:mm:ss for storage and responses. Fractional seconds are discarded because MagicBase currently stores datetime values with second precision. Date and datetime filter values follow the same format rules.
Do not manually remove Z or a numeric timezone offset before calling createRow, batchCreateRows, updateRow, or a query filter. Removing timezone information changes the represented instant instead of converting it correctly.
Model UI choices with MySQL-like columns:
- Single-choice UI values use
text.
- Multiple-choice UI values use
json and write an array, such as ["office", "gaming"].
- User IDs, department IDs, attachment IDs/URLs, and foreign-key values use
text unless the app truly needs a JSON array/object.
- Do not use low-code field types such as
single_select, multi_select, user, department, attachment, or reference; MagicBase no longer exposes them as column types.
- Relations are separate metadata. Create ordinary key columns such as
customer_id: text, then create a MagicBase relation between source and target columns when joined reads are needed.
Current User Context (window.Magic.getContext)
Use window.Magic.getContext() whenever a micro-app needs current-user display, creators, owners, assignees, collaborators, "my data versus all data", or edit/delete permission checks.
getContext() is hosted by the parent application. It uses the current login state to query magic-service user information and returns a normalized current-user profile. HTML business code should not call /api/v1/contact/users/queries directly for the current user, should not hard-code tokens, and should not read .credentials files.
const context = await window.Magic.getContext();
For data apps with ownership or collaboration:
- Ensure
app.json.anonymous matches the permission model. Apps that depend on real current-user identity, created_by, owner-only editing, departments, organizations, or team collaboration must set anonymous:false. Anonymous apps can only use identity-independent public flows such as public display or anonymous intake.
- Call
window.Magic.getContext() during initialization before user-dependent reads or writes.
- Prefer MagicBase system fields for ownership. MagicBase automatically records
created_by for every row, and backend private_user permissions are based on this system field.
- Do not create a dynamic
creator_user_id column just to enforce creator permissions. Create business identity fields only when the app needs UI display, filtering, assignment, or domain-specific ownership beyond the system creator, such as creator_name, owner_user_id, owner_name, assignee_user_id, or updated_by_user_id.
- When creating a row, write only real business fields. Do not write system fields such as
created_by, organization_code, created_at, or updated_at.
- When rendering edit/delete/archive/transfer buttons for creator-owned rows, request
created_by through select and compare it with context.userId. Display names are not permission keys.
- If
getContext() fails, disable user-dependent create, edit, delete, ownership, and transfer operations and show an understandable error. Never write fake identities such as unknown, guest, visitor, 访客, or 未命名用户 into MagicBase.
Micro-app Administrator Pages
Use this pattern when a micro-app has pages such as an administrator dashboard, aggregate statistics, exports, or management tools that ordinary users must not open:
- Set
app.json.anonymous to false; administrator checks require a real login.
- Put a shared
app.js in the app root and declare page-relative paths:
window.MagicAppConfig = {
admin_pages: ["admin.html", "statistics.html"],
};
- Call
window.Magic.db.getProjectAdminAccess() during startup. It is a host-bridged request to GET /api/v1/magicbase/projects/{projectId}/admin-access, so the host supplies the current project context, login authorization, and share token when applicable.
- The backend evaluates the real logged-in user. A share token only proves access to the current project share and must never be used as the current user or share creator identity.
- Hide administrator links on the home page when
is_admin is false. Every administrator page must repeat the guard before loading its protected data; direct URL access must not bypass it.
- Do not call administrator data endpoints before the guard resolves. Frontend hiding is not a substitute for MagicBase dynamic/static permissions or a backend authorization check.
Recommended guard:
window.MagicAppGuard = {
async requireAdminPage(pagePath) {
const pages = window.MagicAppConfig?.admin_pages || [];
if (!pages.includes(pagePath)) return true;
const access = await window.Magic.db.getProjectAdminAccess();
if (!access?.is_admin) {
window.location.replace("index.html");
return false;
}
return true;
},
};
If a requested administrator rule depends on workflow state, cross-table membership, approval, hierarchy, time windows, quotas, or sensitive business validation, classify it as requires_backend; admin_pages only gates pages and does not implement those business rules.
Recommended pattern:
let context = null;
async function initRuntime() {
context = await window.Magic.getContext();
if (!context?.userId || !context?.userName) {
throw new Error("Current user information is unavailable");
}
}
async function createTask(data) {
if (!context) {
throw new Error("Current user information is not initialized");
}
return window.Magic.db.createRow(
TASK_TABLE_ID,
{
...data,
creator_name: context.userName,
},
["id", "title", "status", "created_by", "creator_name", "created_at", "updated_at"]
);
}
function canEdit(row) {
return Boolean(context?.userId && row.created_by === context.userId);
}
MagicBase Dynamic Permissions
MagicBase dynamic_permissions are the backend security boundary. Frontend filters, hidden buttons, and canEdit() checks are only product experience safeguards. They must not be treated as the only permission control when the user asks for private, owner-only, organization-shared, department-shared, read-only, or restricted-edit data.
Before creating a table for a multi-user data app, split the permission intent into four questions and reflect the answer in the micro_app_plan assumptions and create_magicbase_table.dynamic_permissions:
- Who can read rows?
- Who can insert rows?
- Who can edit rows?
- Who can delete rows?
- Is the permission rule enforceable by MagicBase scopes/static permissions, or does it require backend business logic?
If the user gives partial permission intent, infer the missing parts with the least-privilege default that still satisfies the product loop, and state the assumption in the plan. Do not omit dynamic_permissions for data that has ownership, privacy, collaboration, organization, department, review, or read-only semantics.
Pass dynamic_permissions as a nested object in the tool arguments, not as a JSON string. Do not stringify it or wrap the object in quotes. If the tool rejects dynamic_permissions with "expected object", retry with the same permission intent as an object; do not remove the permission field or fall back to a public table.
Permission feasibility boundary
Before promising a permission feature, classify it in the plan as one of:
enforceable_by_magicbase: MagicBase can enforce it with table, row, column, and static permissions.
ui_only_not_secure: The HTML app can hide buttons, disable fields, or filter lists, but users could bypass the UI and call MagicBase directly. This is product guidance only, not security.
requires_backend: The rule needs custom backend logic or an extension to the MagicBase permission model. Do not generate a front-end-only app while claiming the permission is secure.
When the request contains ui_only_not_secure or requires_backend rules, tell the user before building or changing schema. If the user accepts a UI-only downgrade, state the downgrade explicitly in micro_app_plan.assumptions and do not describe it as enforced permission.
MagicBase can enforce these common cases:
- Table-level read/insert/manage access.
- Row access by built-in scopes:
public, private_user, private_department, private_org, disabled.
- Creator-owned rows through system
created_by.
- Organization/department sharing where the rule directly matches MagicBase scopes.
- Static grants to explicit users, departments, organization, or anonymous subjects for fixed tables, columns, or rows.
- Column read/edit restrictions that do not depend on row state or cross-table business logic.
MagicBase cannot enforce these cases in a pure HTML + MagicBase micro-app:
- State-dependent permissions such as "only approvers can edit while pending, everyone read-only after approved".
- Cross-table or relation-computed permissions such as "only users listed in the project members table can edit that project's tasks".
- Hierarchical business relationships such as "a direct manager can edit subordinate records".
- Conditional field permissions such as "finance can edit amount only when amount is greater than 10000".
- Time-window, quota, sequence, or workflow permissions such as "editable only within 10 minutes", "submit at most 3 times per day", or "must complete A before editing B".
- Sensitive domain operations that require authoritative validation, such as payments, approvals, inventory deduction, financial balance changes, points, credits, or settlement.
For those cases, use wording like:
This permission depends on business logic that MagicBase cannot enforce in a pure front-end micro-app. I can implement UI hints such as hidden buttons or disabled fields, but that is not secure permission because users may bypass the UI and call MagicBase directly. To enforce it, add a backend endpoint or extend the MagicBase permission model.
Scope selection:
public: public collaborative data that every permitted project user may access.
private_user: only the row creator may read, edit, or delete. This uses MagicBase system created_by, not a dynamic creator_user_id column. Use this for personal records, my applications, my drafts, owner-only edit/delete, and creator-owned rows.
private_department: users may access rows created by people in overlapping departments. Use only when the user explicitly asks for department-level isolation.
private_org: users in the same organization may access the data. Use for organization-shared data.
disabled: dynamic permissions do not grant access; use only when the app intentionally relies on static permissions or administrators.
Permission intent matrix:
| User intent | Table scope | Row scope |
|---|
| Everyone collaborates and can edit/delete all rows | read_scope=public, insert_scope=public | read_scope=public, edit_scope=public, delete_scope=public |
| Everyone can read, only the creator can edit/delete | read_scope=public, insert_scope=public | read_scope=public, edit_scope=private_user, delete_scope=private_user |
| Each user can only access their own rows | read_scope=public, insert_scope=public | read_scope=private_user, edit_scope=private_user, delete_scope=private_user |
| Organization-shared rows | read_scope=public, insert_scope=public | Use private_org for read/edit/delete only where the user asks for organization-wide access; otherwise combine read_scope=private_org with edit_scope=private_user and delete_scope=private_user for creator-managed org data. |
| Department-shared rows | read_scope=public, insert_scope=public | Use private_department for read/edit/delete only where the user asks for department-wide access; otherwise combine read_scope=private_department with edit_scope=private_user and delete_scope=private_user for creator-managed department data. |
| Public intake form where submitters should not edit after submit | read_scope=public, insert_scope=public | Do not grant ordinary public edit/delete. Use edit_scope=disabled, delete_scope=disabled, or explain that administrator/explicit permissions are required for later review operations. |
| Admin-maintained data with ordinary user read-only access | read_scope=public, insert_scope=disabled unless users may submit |
Correct create_magicbase_table pattern for a todo app where everyone can read all todos but only the creator can edit or delete:
{
"table_key": "tasks",
"table_name": "Tasks",
"description": "Team-visible todo tasks with creator-only edits",
"columns": [
{
"column_key": "title",
"column_name": "Title",
"data_type": "text",
"is_required": true
},
{
"column_key": "status",
"column_name": "Status",
"data_type": "text",
"is_required": true,
"default_value": "pending"
}
Field-level permissions:
- System fields such as
created_by, organization_code, created_at, and updated_at are not dynamic columns. They can be selected where supported, but must not be written in createRow or updateRow.
- Business ownership, assignment, review, status-transition, audit, and statistics fields should not be publicly editable unless the user explicitly asks for open collaboration.
- If a field is for display only, derived data, review state, ownership, or system-like metadata, either set an appropriate
dynamic_permission for the column or do not render an edit control for it. Backend row permissions are still the security boundary.
If query_magicbase_tables or get_magicbase_table finds an existing table whose row permissions do not match the user's permission intent, use update_magicbase_table_permissions before claiming backend enforcement. Do not describe frontend filtering, hidden buttons, or disabled controls as secure permission enforcement unless MagicBase dynamic permissions are updated to match the requirement.
Destructive schema changes:
delete_magicbase_table and delete_magicbase_column are destructive. Use them only after the user explicitly confirms the deletion in the approved plan or follow-up instruction.
update_magicbase_column expects the complete desired column definition. Call get_magicbase_table first, preserve unchanged values, and then pass the final column_key, column_name, data_type, is_required, default value, and any field dynamic_permission that should remain in effect.
Agent-side row data operations:
- These tools execute through Magic Service with the current session user's
Authorization, organization, and project context. They are subject to the same project role, table, row, column, dynamic, and static permission checks as ordinary MagicBase calls. They are not service-account or administrator bypasses.
- The project identity comes from the tool execution context and is checked against the persisted session. Never ask the model or user to provide
project_id, authorization, organization_code, a share token, created_by, or other actor fields as tool arguments.
- Use
create_magicbase_row or batch_create_magicbase_rows only when the user explicitly asks to import, initialize, backfill, or enter real business data. Do not insert demo rows merely to populate a generated page or verify CRUD.
- Batch creation accepts at most 200 rows per call. All rows are validated before the batch write begins. Pass only dynamic business fields from the table schema; MagicBase writes system fields automatically.
- Before deleting, call
query_magicbase_rows to resolve the exact real row IDs and show the user the deletion scope. delete_magicbase_row and batch_delete_magicbase_rows require confirm_delete=true after explicit user confirmation.
- Agent-side row mutations never update
.magicbase/migrations.json or the MagicBase data-model section in MICRO-APP.md; those files describe schema, not business records.
MagicBase Runtime Database API (window.Magic.db)
The HTML runtime database API only supports row-level operations on existing MagicBase tables. It does not create tables, create columns, or manage schema.
It also exposes the read-only administrator check for the current project:
const access = await window.Magic.db.getProjectAdminAccess();
This check uses the real logged-in user from Authorization. In a shared micro-app, the host may also send the share token so the backend can verify the shared project, but the share token/share creator is never treated as the current user.
For data-oriented micro-apps, treat MagicBase persistence as the default. Surveys, forms, todos, CRUD apps, small admin panels, dashboards, trackers, and any app with user-submitted, editable, collected, analytical, searchable, exportable, or reusable data should prepare a MagicBase table before generating HTML. Skip persistence only for pure showcase/static pages, pure calculators, apps with no user data, or when the user explicitly says not to save data.
The data model must serve the full approved product loop, not the smallest possible CRUD shell. Derive fields from the planned object, attributes, lifecycle state, category/grouping needs, notes/details, ordering, archive/deletion behavior, statistics, and filters. UI-only state should stay in JavaScript; anything needed for persistence, search, filtering, sorting, or later reuse belongs in MagicBase.
System fields are not dynamic business columns. MagicBase automatically maintains fields such as id, record_id, created_at, updated_at, created_by, project_id, table_id, and organization_code. HTML code may read, display, select, filter, or sort by supported system fields, but must not put system fields into the data object passed to createRow or updateRow. Only dynamic business fields that appear in the table's columns list as column_key may be written in data.
When select is omitted or empty, row APIs return all readable dynamic business fields plus these default row system fields: id, record_id, organization_code, created_at, updated_at, and created_by. When select is provided, the response is an exact projection: include every business or system field the UI needs.
When UI behavior depends on creator ownership, default queries already include created_by. If you provide an explicit select for queryRows, getRow, createRow, or updateRow, include id, all displayed business fields, created_by, created_at, and updated_at in that select. Compare row.created_by with context.userId; do not require a dynamic creator_user_id field unless the product has a separate business owner concept.
Canonical runtime signatures:
window.Magic.db.queryRows(tableId, query)
window.Magic.db.createRow(tableId, data, select?)
window.Magic.db.updateRow(tableId, recordId, data, select?)
window.Magic.db.deleteRow(tableId, recordId)
Never omit the first tableId argument. In particular, updateRow(recordId, data, select) is wrong and will not update the intended MagicBase table.
- Call
query_magicbase_tables to check whether the required table already exists.
- If the table is missing, call
create_magicbase_table; the tool automatically records migration history in .magicbase/migrations.json and refreshes the latest MagicBase data model in MICRO-APP.md.
- If columns are missing, call
create_magicbase_column; the tool automatically records migration history in .magicbase/migrations.json and refreshes the latest MagicBase data model in MICRO-APP.md.
- Generate HTML only after you have a real
table.id from MagicBase tools or from a successful reconciliation against MagicBase.
Never pass table_key or table_name as tableId to window.Magic.db. The HTML code must use the real table id returned by MagicBase tools.
Database API calls are automatically associated with the current project inside the iframe, so HTML code does not pass projectId.
List tables getTables()
const tables = await window.Magic.db.getTables();
- Return:
Promise<Array<{ id: string; name: string; ... }>> — table summaries
Get table details getTable(tableId)
const table = await window.Magic.db.getTable(TABLE_ID_FROM_MAGICBASE_TOOL);
- Parameters:
tableId: string — 表 ID
- Return:
Promise<object> — table details, including field definitions
Create a row createRow(tableId, data, select?)
const newRow = await window.Magic.db.createRow(TABLE_ID_FROM_MAGICBASE_TOOL, {
name: "Alice",
age: 30,
email: "alice@example.com",
}, ["id", "name", "email", "created_at", "updated_at"]);
- Parameters:
tableId: string、data: Record<string, unknown>、select?: string[](可选,指定返回字段)
- Return:
Promise<object> — the created row
- The
data object must contain only dynamic column keys from the actual table schema. Do not write created_at, updated_at, id, record_id, created_by, project_id, table_id, or organization_code into data; omit select for the default response fields or request specific system fields through select if you need a custom projection.
- Match values to the MySQL-like column type. For
json columns, pass arrays/objects directly; do not stringify or join arrays before calling createRow.
Query rows queryRows(tableId, query)
const result = await window.Magic.db.queryRows(TABLE_ID_FROM_MAGICBASE_TOOL, {
filter: { name: { eq: "Alice" } },
sort: [{ field: "created_at", order: "desc" }],
select: ["name", "email"],
page: 1,
page_size: 20,
});
const rows = result.list;
- Parameters:
tableId: string、query: object
filter? — 过滤条件。Use MagicBase operators without $: equality is { field: { eq: value } }, inclusion is { field: { in: [value1, value2] } }. Do not use Mongo operators such as $eq, $in, or $contains; unsupported operators are ignored by the current backend.
sort? — 排序规则。Use { field: "created_at", order: "desc" }; the backend reads order, not direction.
select?: string[] — 返回字段列表
page?: number — 页码(默认 1)
page_size?: number — 每页行数(默认 20)
with? — 关联查询配置
- Return:
Promise<{ list: Array<object>; total: number; page: number; page_size: number }> — 分页结果。行数组字段是 list,不要使用 rows
- 超时:30 秒(其他操作为 15 秒)
- If
select is omitted or empty, each row includes all readable dynamic business fields plus id, record_id, organization_code, created_at, updated_at, and created_by.
- If the UI enables or disables actions based on creator ownership and you provide an explicit
select, it must include id, the displayed business fields, created_by, created_at, and updated_at.
Use this defensive read pattern when handling existing or uncertain runtime responses:
const result = await window.Magic.db.queryRows(TABLE_ID_FROM_MAGICBASE_TOOL, {
page: 1,
page_size: 100,
});
const rows = Array.isArray(result?.list)
? result.list
: Array.isArray(result?.data?.list)
? result.data.list
: [];
Prefer result.list in new code. The result.data?.list fallback is only a compatibility guard for uncertain host responses. Do not use result.rows.
获取单行 getRow(tableId, recordId, select?)
const row = await window.Magic.db.getRow(TABLE_ID_FROM_MAGICBASE_TOOL, "rec_yyy");
- Parameters:
tableId: string、recordId: string、select?: string[]
- Return:
Promise<object> — 行数据
Update a row updateRow(tableId, recordId, data, select?)
const updated = await window.Magic.db.updateRow(TABLE_ID_FROM_MAGICBASE_TOOL, "rec_yyy", {
name: "Bob",
age: 25,
}, ["id", "name", "age", "updated_at"]);
- Parameters:
tableId: string、recordId: string、data: Record<string, unknown>、select?: string[]
- Return:
Promise<object> — the updated row
- Do not call
updateRow(recordId, data, select). The first argument must always be the real MagicBase table id, followed by the row/record id.
- The
data object must contain only dynamic column keys from the actual table schema. Do not write system fields such as updated_at; MagicBase updates them automatically and they can be returned through select.
- Match values to the MySQL-like column type. For
json columns, pass arrays/objects directly; do not stringify or join arrays before calling updateRow.
Delete a row deleteRow(tableId, recordId)
await window.Magic.db.deleteRow(TABLE_ID_FROM_MAGICBASE_TOOL, "rec_yyy");
- Parameters:
tableId: string、recordId: string
- Return:
Promise<void>
获取relation list getRelations()
const relations = await window.Magic.db.getRelations();
- Return:
Promise<Array<object>> — 当前项目的表relation list
Database error handling
try {
const row = await window.Magic.db.getRow(TABLE_ID_FROM_MAGICBASE_TOOL, "rec_notfound");
} catch (err) {
console.error("Database operation failed:", err.message);
}