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.
70
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
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:
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.query_magicbase_rows, create_magicbase_row, batch_create_magicbase_rows, delete_magicbase_row, and batch_delete_magicbase_rows.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.
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.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:
text.json and write an array, such as ["office", "gaming"].text unless the app truly needs a JSON array/object.single_select, multi_select, user, department, attachment, or reference; MagicBase no longer exposes them as column types.customer_id: text, then create a MagicBase relation between source and target columns when joined reads are needed.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();
// context: {
// userId: "usi_xxx",
// userName: "Alice",
// user: { user_id, real_name, nickname, avatar_url, phone, email, ... },
// organizationCode: "org_xxx",
// language: "zh-CN"
// }For data apps with ownership or collaboration:
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.window.Magic.getContext() during initialization before user-dependent reads or writes.created_by for every row, and backend private_user permissions are based on this system field.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.created_by, organization_code, created_at, or updated_at.created_by through select and compare it with context.userId. Display names are not permission keys.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.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:
app.json.anonymous to false; administrator checks require a real login.app.js in the app root and declare page-relative paths:window.MagicAppConfig = {
admin_pages: ["admin.html", "statistics.html"],
};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.is_admin is false. Every administrator page must repeat the guard before loading its protected data; direct URL access must not bypass it.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, // UI display only; backend ownership uses system created_by.
},
["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 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:
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.
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:
public, private_user, private_department, private_org, disabled.created_by.MagicBase cannot enforce these cases in a pure HTML + MagicBase micro-app:
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 | Do not use public edit/delete. Use edit_scope=disabled, delete_scope=disabled, or explicit manager/admin permissions. |
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"
},
{
"column_key": "creator_name",
"column_name": "Creator Name",
"data_type": "text",
"is_required": false
}
],
"dynamic_permissions": {
"table": {
"read_scope": "public",
"insert_scope": "public"
},
"row": {
"read_scope": "public",
"edit_scope": "private_user",
"delete_scope": "private_user"
},
"columns": {}
}
}Field-level permissions:
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.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:
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.project_id, authorization, organization_code, a share token, created_by, or other actor fields as tool arguments.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.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..magicbase/migrations.json or the MagicBase data-model section in MICRO-APP.md; those files describe schema, not business records.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();
// { project_id: "...", is_admin: true|false }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.
query_magicbase_tables to check whether the required table already exists.create_magicbase_table; the tool automatically records migration history in .magicbase/migrations.json and refreshes the latest MagicBase data model in MICRO-APP.md.create_magicbase_column; the tool automatically records migration history in .magicbase/migrations.json and refreshes the latest MagicBase data model in MICRO-APP.md.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.
getTables()const tables = await window.Magic.db.getTables();
// tables: [{ id: "1234567890", name: "users", ... }, ...]Promise<Array<{ id: string; name: string; ... }>> — table summariesgetTable(tableId)const table = await window.Magic.db.getTable(TABLE_ID_FROM_MAGICBASE_TOOL);
// table: { id: "1234567890", name: "users", fields: [...], ... }tableId: string — 表 IDPromise<object> — table details, including field definitionscreateRow(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"]);
// newRow: { id: "rec_yyy", name: "Alice", age: 30, ... }tableId: string、data: Record<string, unknown>、select?: string[](可选,指定返回字段)Promise<object> — the created rowdata 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.json columns, pass arrays/objects directly; do not stringify or join arrays before calling createRow.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,
});
// result: { list: [...], total: 42, page: 1, page_size: 20 }
const rows = result.list;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? — 关联查询配置Promise<{ list: Array<object>; total: number; page: number; page_size: number }> — 分页结果。行数组字段是 list,不要使用 rowsselect 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.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");tableId: string、recordId: string、select?: string[]Promise<object> — 行数据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"]);tableId: string、recordId: string、data: Record<string, unknown>、select?: string[]Promise<object> — the updated rowupdateRow(recordId, data, select). The first argument must always be the real MagicBase table id, followed by the row/record id.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.json columns, pass arrays/objects directly; do not stringify or join arrays before calling updateRow.deleteRow(tableId, recordId)await window.Magic.db.deleteRow(TABLE_ID_FROM_MAGICBASE_TOOL, "rec_yyy");tableId: string、recordId: stringPromise<void>getRelations()const relations = await window.Magic.db.getRelations();Promise<Array<object>> — 当前项目的表relation listtry {
const row = await window.Magic.db.getRow(TABLE_ID_FROM_MAGICBASE_TOOL, "rec_notfound");
} catch (err) {
console.error("Database operation failed:", err.message);
// 可能的错误:
// - "No project selected" — 未选中项目
// - "getRow: tableId must be a non-empty string" — 参数校验失败
// - HTTP 错误信息(404, 500 等)
}f9973c5
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.