Single backend skill for Taruvi. Covers two layers:
- Control plane (MCP) — provisioning datatables, policies, roles, buckets, secrets, function metadata, analytics queries, audited raw SQL.
- Data plane (function runtime) — Python bodies with signature
def main(params, user_data, sdk_client)running on Celery workers with a pre-authenticatedsdk_client.
If the task is wiring React/Refine providers or hooks on the frontend, switch to taruvi-refine-providers.
Decision: function or just MCP provisioning?
Provisioning alone is enough for: schema changes, policy/role/user management, bucket creation, secret management, registering analytics queries, one-shot admin SQL.
You need a function body when the task involves any of:
- 2+ resources at runtime (DB + storage, users + DB, etc.)
- Event triggers (
RECORD_CREATE,RECORD_UPDATE,RECORD_DELETE,POST_USER_CREATE, …) - Schedule / cron
- External API call with a stored secret
- Long-running work (>30s →
async_mode=True) - Public unauthenticated endpoint (
is_public=True) - Authorization gate beyond plain Cerbos resource policy
- Function-to-function pipeline
When in doubt, see when-not-to-use-functions.md.
Workflow
- Read
architecture-overview.mdonce for the runtime split; for greenfield apps also scaffold anAGENTS.mdfromagents-md-template.md. - Decide function vs provisioning (above). Before writing function code, read
function-authoring.mdand the relevant section offunction-sdk-reference.md. - For MCP work, open
mcp-tool-quickref.mdand the task-specific reference below. - For destructive ops, follow the destructive-op protocol (below).
- Verify by re-reading the resource (
get_datatable_schema,manage_policies(action="get"), etc.) and executing functions / analytics queries end-to-end before reporting done.
Reference index
| Task | Read |
|---|---|
| MCP tool signatures | mcp-tool-quickref.md |
| Frictionless schema (FKs, indexes, hierarchy, graph) | datatable-schema-patterns.md |
| Cerbos policy authoring | cerbos-policy-cookbook.md — or get_ai_docs(category="policies", topic="guide") |
| Secrets and custom secret types | secrets-and-types.md |
| Audited raw SQL | raw-sql-safety.md |
| Analytics queries (registration + Jinja2 params) | analytics-queries.md |
| Filter operators / query capabilities | backend-capabilities.md |
| Function runtime contract, modes, triggers | function-authoring.md |
sdk_client modules and methods |
function-sdk-reference.md |
| Event triggers, CEL filters | function-events.md |
| Worked function examples (8 scenarios) | function-scenarios.md |
| Cross-layer worked features | feature-workflow-examples.md |
| Integration pitfalls | integration-pitfalls.md |
| Frontend Worker deploy | frontend-worker-deploy.md |
| Export backend config | backend-export.md |
| Env var setup | env-setup.md |
Non-negotiables
Most of these are wired into the references too — listed here because skipping them causes real bugs.
- Trust the MCP tool, not memory. Tool responses carry the IDs/slugs/status you need next. Don't invent endpoints or method names.
create_update_schemadrops fields missing from the payload. Alwaysget_datatable_schemafirst, then send the full preserved field list.manage_policies(action="create_update")replaces — does not merge. Get the existing policy first, mutate, and send the full body back.- Function signature is exactly
def main(params, user_data, sdk_client):. Any deviation → immediateSandboxError. - Never re-authenticate
sdk_client. It's pre-authenticated. Noclient.auth(), noclient.login(), no API keys passed in. - Never hardcode secrets. Use
sdk_client.secrets.get("KEY"). - Return JSON-serializable values from functions.
datetime,set,Decimal, and custom classes crash on return — convert first. - Tasks >30s need
is_async=True. Sync calls time out at 30s and leave UI spinners stuck. - Use
log(), notprint().print()is unstructured stdout and not queryable. - Frontend multi-resource cascades are bugs. Move them to a function — browser navigation mid-operation leaves the DB inconsistent.
Destructive-op protocol
Applies to: delete_datatable (especially force=True), manage_policies(action="create_update") on an existing policy, user_attributes_schema(action="update"), manage_secret_types(action="delete"), manage_function(action="delete"), manage_roles(action="delete"), and any execute_raw_sql containing DROP, TRUNCATE, destructive ALTER, or DELETE without WHERE.
- Plan — inspect current state (
get_datatable_schema,manage_policies(action="get"), …) and state what will be deleted/replaced and what depends on it. - Validate — surface the blast radius in plain language ("This will drop
ordersand 3 dependent FKs ininvoices"). Get explicit user confirmation. - Execute — only after confirmation. Report the tool's response verbatim.
Test users — always
After provisioning, create test users so the user can verify the app works:
- With access control: one test user per role, named
qa_<role_slug>_<YYYYMMDD>(e.g.,qa_warehouse_staff_20260422), strong 12+ char password (mixed case + digit + symbol). Report all usernames and passwords back to the user. - No access control: one default super-admin test user, password reported.
- Include cleanup guidance: deactivate/delete the
qa_*users after validation, or rotate their passwords.
Verify by executing — always
Before reporting a function or analytics query done:
- Run
execute_function(function_slug=..., params={...})orexecute_query(query_slug=..., params={...})with realistic sample params. - Inspect the actual response shape.
- Confirm any frontend code uses the exact field names and structure returned — fix the frontend if they don't match.
Drift check
node scripts/check-versions.js warns when pinned SDK/provider versions drift from the latest on PyPI/npm.