- Treat this file as the primary instruction source for every task in this repository.
- After context compaction, summarization, restart, or loss of working context, read this file again before you continue.
- Preserve a reference to
CLAUDE.mdin a handover or context summary so the next context reloads it. - Do not rely on remembered repository rules when this file is available.
Central is the control plane for Frappe Cloud v2. It owns identity, teams, capabilities, regions, catalog, billing, and managed services. It contains a Frappe app and a Vue dashboard.
Read the README for setup and local development. Read spec/README.md for the specification router, and read the matching specification before you make a structural change.
- IAM: identity, permissions, and Atlas enforcement.
- Capabilities: the authorization vocabulary and its plane split.
- Integrations: the contracts with Atlas, Pilot, and Cargo.
Central is one of three main repositories for cloud v2. Keep behavior in the repository that owns it.
Central control plane: identity, teams, capabilities, catalog, billing, regions
| HTTP + signed tokens
v
Atlas regional runtime: VMs, network, proxy. One instance per region.
|
v
Pilot server runtime: benches, sites, apps on a single server.
- Central decides every
server:*capability itself. A region only checks that a signed request's tenant matches the resource's tenant; a bench checks the scope on its own signed token. Neither holds a capability model of its own. - Central reaches Atlas, Pilot, and Cargo only through the clients in
central/integrations/. Do not call a remote plane from a controller, an API route, or a page. Virtual Machineis Central's server record. Central owns its identity, title, plan, image and billing links. A region only reports state back, and the integration layer applies that throughVirtualMachine.record_observed_state.
Central runs today:
- Identity and access: teams, members, invitations, team roles, and capabilities.
- Tokens: SSO and OAuth minting for Atlas, for a Pilot bench, and for Cargo, Datum and for any other future services, plus site login.
- Regions: each Region holds its Atlas connection and its Cargo connection.
- Resources:
Virtual Machinefor a provisioned server,Sitefor a self-serve site. Only the integration layer records observed state on them. - Provisioning: durable
Resource Actionrecords that Central processes against Atlas and Pilot. - Managed services: team services and service details, LLM models and plan policies, and object storage.
- Notifications: event types, team notifications, user preferences, and the delivery engine.
- Billing: catalog, subscriptions, invoicing, payments, credits, and projections.
- The console in
dashboard/.
Central plans to add:
- One
SPEC.mdper module, written as each module is rewritten. - A repository layout redesigned from first principles, to match the current Atlas and Pilot boundaries.
central/billing/** is out of scope for the rewrite. Change it only when the task names it, and keep the change as small as the task needs.
- Keep changes small, direct, and in the module that owns the behavior.
- Keep decision-making visible to the user. Before implementation, explain the proposed design, assumptions, ownership, state, dependencies, error flow, risks, and important trade-offs. Use a small ASCII diagram when it helps. Proceed after the user agrees with the plan.
- Build the minimum working change, then iterate. Delete before you add when existing code can be simplified.
- For a bug fix, find the root cause before you change code.
- Do not change unrelated dirty files, generated artifacts, or local data.
- Do not add plan files such as
plan_*.md. Track planned work in GitHub issues. - Do not commit secrets, private keys, tokens,
.envcontent, or production credentials. - Use
git mvwhen you intentionally move or rename a tracked file.
Central runs as an internal release. Keep backward compatibility minimal.
- Add a data patch for a change to stored data. Do not leave old data behind a new schema.
- Do not add compatibility layers, deprecated aliases, or fallback behavior unless the task or specification requires them.
- Do not design for rolling upgrades, mixed-version deployments, or zero-downtime migration unless required.
Central is the authorization source for the whole control plane. Treat every permission change as security-sensitive.
Access is team-based, not document-based. A user never holds a grant on a document. A user holds a role in a Team, the role carries capabilities, and a document is reachable because it belongs to a Team the user holds the right capability in.
User -> Team Member -> Team Role -> Role Capability -> Capability ("server:view")
|
Document (.team field) -------------------------------------+
- Resolve every decision through
central/iam.py. Usecan(user, team, capability),get_user_team_names_with_capability, andresolve_team. Do not read Team Member, Team Role, or Role Capability rows anywhere else. - Give every team-scoped DocType an indexed
teamlink field. A document without a team is unreachable by design, and a query condition must return1 = 0rather than fall open. - Scope a list with the team set for the capability, not with team membership alone. Two users in the same Team can differ on
server:view. - Keep
System Manageras the only operator bypass, throughuser_has_operator_bypass. - Add a capability to
CAPABILITIES.mdand the fixtures in the same change. Never invent a capability string at a call site.
Every team-scoped DocType needs a permission_query_conditions entry and a has_permission entry, wired in hooks.py and named <doctype>_query_conditions and <doctype>_has_permission. Virtual Machine uses the server_ prefix. A query condition alone still leaks a single document by name. A has_permission alone still leaks the list. Reuse _team_field_query_conditions and _team_field_has_permission instead of writing a new pair by hand.
An API route may still check can(...) before it acts, because a route must fail with a clear message and must gate writes. That check is a second layer, not the only one. Do not let it become the only thing standing between a user and another team's data.
- Use
frappe.get_listwhen the read serves a request on behalf of a user. It applies the query conditions, so the framework enforces the team boundary even when a route forgets to. - Use
frappe.get_allonly on a system path: a patch, a background job, a scheduled task, the integration layer, a test, or a read that runs as the system rather than as a user. Keep the explicitteamfilter there as well. - Do not reach for
ignore_permissions=True. Model the roles and capabilities correctly instead. When a call must bypass permissions, put one comment line above it that states why. - Give each permission function a short docstring that lists its rules in order, one numbered rule per line.
- Add a test in
central/tests/for every new rule, including the denial case and the cross-team case.
The console serves customers. Desk serves the operator who has to answer a page at 2 am. Build every DocType so that person can read it, find it, and act on it without the console and without a shell.
- Mark the hot columns
in_list_view: the identity, the owning Team, the state, and the one number or timestamp that says whether the record is healthy. - Mark the fields an operator filters on
in_standard_filter: status, team, region or cluster, and type. - Index the fields that the list filters and sorts on. Add them in the controller's
on_doctype_update, not in a patch. - Set a meaningful
title_fieldandsearch_fields. An operator searches by the name a customer gives them, not by a hash. - Give a state field an indicator colour so the list reads at a glance.
- Group fields into tabs and sections that follow the operator's task, not the table order. A form that is one flat column of 40 fields is a defect.
- Put identity and state at the top. Put credentials, raw payloads, and debug fields in a collapsed section or a separate tab.
- Label a field with what it means to a person. Set a description on any field whose meaning is not obvious from the label.
- Mark a field read-only when only the system writes it. An observed field must never look editable.
- Add dashboard connections to every DocType an operator would follow next. From a Team reach its sites, servers, and invitations. From a Virtual Machine reach its actions, tasks, and events. An empty
linksarray on a hub DocType is a gap. - Link related records with a Link field rather than a plain text identifier, so the connection is reachable in both directions.
- Add a desk Action button in the DocType client script for any operation an operator performs by hand today. Retry a failed task, resend an invitation, force a reconcile, rotate a credential, cancel a stuck provisioning request.
- Show the button only when the action is valid for the current state and the current user, and confirm anything destructive.
- Put the behavior in the server method that owns it and let the button call it. The button is a trigger, not a place for logic.
- Surface failure where the operator is looking. A failed background task must leave a readable error on the record, not only in a log.
- Choose clear code over clever code. Prefer explicit configuration over implicit behavior.
- Use complete words for names. Avoid abbreviations.
- Put behavior with the module, domain object, or DocType controller that owns it.
- Use standard Frappe APIs and existing repository helpers before you add custom logic or a dependency.
- Keep functions small. About 25 lines is a useful target, not a reason to split readable code.
- Put blank lines between logical code blocks in a function.
- Use a comment of 1 or 2 lines where the purpose needs explanation. Explain why, not how.
- Keep cyclomatic complexity at 8 or less. Keep files between 100 and 500 lines when practical.
- Extend an existing folder before you add a new one. Group related files instead of adding more same-prefix modules.
- Do not add generic
utils,helpers,common, ormiscfolders. - Keep one owner for state that can drift. Keep temporary state inside the object or module that owns its lifecycle.
- Fail near the cause. Do not hide corrupt or partial state behind a broad fallback. Retry only operations that are safe to repeat.
- Do not add comments that repeat the code. Explain a business rule, invariant, external quirk, or concurrency rule only when the code cannot show it.
- Do not put an explanatory comment at the top of a file. Use a short class or function docstring.
- Add focused tests for changed behavior and failure cases. Do not add tests only to raise coverage. Keep tests deterministic and independent.
- Follow DRY, SOLID, and KISS. Apply them to remove real duplication and real coupling, not to add layers.
- Reduce the layers a reader has to trace. Reduce the state a reader has to hold in their head.
- Don't abstract for the sake of abstraction, abstract only if you can make a distinct concept from it. Every layer should provide a consistent interface and be part of a single task.
- Review every test for it's necessity. If the type system covers it, remove the test. If an existing end to end / integration test covers it, remove it.
- Target Python 3.14 and Frappe v16.
- Keep domain behavior in the DocType controller, service module, or task that owns it. Keep
central/api/routes andhooks.pyentries thin. - Do not add a second API for a variant of an operation that an existing API does. Add an argument to the existing API, for example
invite_team_member(team, invitations=[...])for many people, not a newinvite_team_members. The functions behind the one API can differ. - Use type hints for public functions and important data structures.
- Raise specific exceptions. Handle only errors that the code can recover from. Use
central/errors.pyfor a failure that a person reads. - Prefer standard Frappe APIs and built-in DocTypes over custom machinery. Use
frappe.dbandfrappe.qbinstead of raw SQL. - Wrap every user-facing string in
_(). - Use
better-writingfor backend error messages shown to customers. Keep admin and developer diagnostics outside this copy review. See interface copy guidance. - Avoid mutable global state, circular imports, and lazy re-exports in a package
__init__.py. - Use
@propertyonly for a cheap, side-effect-free, no-argument operation that returns one noun-like value. Useget_<what_it_returns>()for an operation with arguments or multi-step work. - Name boolean properties and methods with
is_orhas_. - Keep a method public when a caller outside its owner uses it. Use a leading underscore for raw parsing, security-sensitive validation, or genuinely internal details. Do not make a method private only because it has one caller.
- Use binary units and name a field for its unit, such as
disk_mib. - Do not reach for
ignore_permissions=True. Model the roles and capabilities correctly instead. When a call must bypass permissions, put one comment line above it that states why. - Use
frappe.qbwith a join when a read spans more than 2 tables. Do not loop a query per row. No N+1 queries. - Add indexes and unique constraints in the controller's
on_doctype_update, not in a patch.
Read the Frappe controller docs before adding a step that reacts to a document reaching a state. When a document follows a lifecycle, put each step in the controller hook that owns that point in the lifecycle, not as a sequence of calls an API route or integration function makes by hand. The common hooks, in the order Frappe calls them:
| Hook | Runs | Use it for |
|---|---|---|
before_validate |
Before validate, on every save |
Normalize input before it is checked |
validate |
Before every save | Enforce invariants; block the save on failure |
before_insert |
Once, before the first save | Set a field a fresh document alone needs |
after_insert |
Once, right after the first save | Kick off what only a newly created document triggers |
on_update |
After every save | React to any change, not only creation |
on_trash |
Before delete | Clean up what the document owns |
A route stays a thin trigger: it builds the document and calls insert() or save(), and the controller's hooks do the rest. This keeps a lifecycle step discoverable from the doctype that owns it instead of buried in whichever route happened to create the document, and it means every path that creates the document (an API route, a patch, a test) gets the same behavior for free.
# Before: the API route wires each step it thinks a new Site needs.
@frappe.whitelist()
def create_trial_site(subdomain: str, team: str) -> dict:
site = frappe.get_doc({"doctype": "Site", "subdomain": subdomain, "team": team})
site.insert()
notify_team_of_new_site(site) # easy to forget on the next caller
return {"name": site.name}
# After: Site.after_insert owns it. Any caller that inserts a Site gets the
# same behavior, and the route no longer needs to know what a new Site does.
class Site(Document):
def after_insert(self) -> None:
notify_team_of_new_site(self)
@frappe.whitelist()
def create_trial_site(subdomain: str, team: str) -> dict:
site = frappe.get_doc({"doctype": "Site", "subdomain": subdomain, "team": team}).insert()
return {"name": site.name}Use Vue 3, TypeScript, and Frappe UI with the Espresso design system.
Organise components in three tiers, and group a feature by domain folder instead of by name prefix.
dashboard/src/components/common/ Domain-agnostic primitives: avatar, loader, skeleton, form field
dashboard/src/components/navigation/ Structural chrome: sidebar and navigation lists
dashboard/src/components/<feature>/ Feature components, one folder per domain
- What can be a common component must be a common component. Move a repeated table action, dialog, empty state, or mutation runner out of the page.
- Keep a page thin. A page composes components and reads a composable. It does not fetch and it does not hold domain logic.
- Put data fetching in a composable under
dashboard/src/composables/. Follow the existing composables. - Keep shared types in
dashboard/src/types/and keep them in step with the DocType they describe. Avoidany. - Give every component typed props with a named props interface.
- Always handle the loading, empty, error, and disabled states. The user must never reach a dead end.
- Use a toast for short success feedback or a non-actionable background status. Use
backgroundErrorToastonly when the failure happened outside the user's current task. - Use
reportErrorfor an API or action failure. It shows one persistent alert at the bottom right of the viewport, where toasts also appear. Give it an action-specific title when the automatic error category does not identify the failed task. - Keep a form or dialog failure inside that form or dialog, before the fields or actions it affects. Put a field validation message directly below its field. Keep a page or list loading failure in its content area.
- Use Frappe UI components and semantic classes for layout, spacing, color, and typography. Use a raw Tailwind class only for what the design system does not cover.
- Do not mix ad hoc Tailwind values with design-system tokens. Inconsistent class usage is a defect.
- Keep line heights and text sizes on the Frappe UI scale.
- Use correct HTML semantics. Give every control a label, a reachable focus state, and keyboard operation.
- Match the pinned Frappe UI version. Report a version mismatch instead of a silent upgrade.
- Use the task-specific interface skill in agent tooling setup. Use
better-writingwhen you add or change empty states, tooltips, labels, helper text, errors, or confirmations.
- Write for the reader who must use, change, or operate Central.
- Use ASD-STE100 Simplified Technical English. Use short, direct sentences and one term for one thing.
- Keep each Markdown prose paragraph on one source line. Do not use em dashes.
- Describe current behavior only. Do not describe removed behavior or old interfaces.
- Put detailed behavior in one authoritative location and link to it from summaries.
- Use headings that answer a reader question, such as Purpose, Configuration, Operation, or Validation.
- Use a list or table for more than 3 related items. Give an example when a command, API, or configuration value can be unclear.
- Update the related documentation in the same pull request as a behavior, interface, operation, or layout change.
- Update
CAPABILITIES.mdand the fixtures together when the capability set changes.
From apps/central, using the Bench Python environment:
../../env/bin/ruff check central
../../env/bin/ruff format central
python3 scripts/check_patches.py
pre-commit run --all-filesFrom the bench root, using Pilot. Pilot finds the bench from the working directory. Add -b <bench> when you run it from elsewhere.
pilot frappe --site central.localhost run-tests --app central
pilot build --apps centralRun a focused module during development and not the entire test suite. If small changes, avoid an entire module and just run the file changes.
pilot frappe --site central.localhost run-tests --app central --module central.tests.test_<name>Use pilot frappe ... for any Frappe CLI command, such as migrate or clear-cache. Use pilot directly for bench-level work, such as build, start, restart, and install-app. Run pilot --help for the full list.
- Mandatory: review every changed line before you commit.
- Remove complexity that the change introduces or exposes, when the removal stays in task scope.
- Keep valid error handling, boundary validation, cleanup, synchronization, and security checks.
- Validate untrusted input at its boundary. Central is the authorization source, so treat every capability and team check as security-sensitive.
- Run the focused tests for the changed module, and the full app suite only before a broad refactor.
- Always run tests in isolation and in a separate site.
- In the handover, report the result, the changed paths, and the verification. Explain implementation details only when the user asks or the reason is not clear.
- Use a short Conventional Commit subject:
type(scope): lower case. - Start the subject after the colon in lower case. Do not end it with a period. Capitalize only a proper noun, such as Atlas or MariaDB.
- Use a kebab-case scope and one of
feat,fix,refactor,test,docs,build, orchore. - Do not add an AI co-author, session data, or agent data.
- Use the same Conventional Commit format for the pull request title.
- Keep the description short and use ASD-STE100 Simplified Technical English.
- Follow the validation and handover rules before you write the description.
- State what changed and why it matters. Do not narrate the implementation.
- Group related changes. Include visual evidence only for visual changes.
- Avoid words like doors, seam, rule etc.
For a bug fix, use this structure:
## Issue
<One sentence that states the user-visible or operational problem.>
## Summary
<1 or 2 sentences that state the fix and why it matters.>
## What changed
- <Specific change>
## Why
<The technical or business reason for this approach.>
## Screenshots
<Before and after evidence. Omit this section when it does not apply.>
## Related issues
Closes #<issue>
For a feature, use this structure:
## Summary
<1 or 2 sentences that state the capability and why it matters.>
## What changed
- <Specific change>
## Why
<The technical or business reason for this approach.>
## Screenshots
<Before and after evidence. Omit this section when it does not apply.>
## Related issues
Refs #<issue>
Use the most specific available skill for the task. See agent tooling setup for sources and installation commands.
| Skill | Use for |
|---|---|
code-style |
Every code edit and code-style question. |
quality-code-review |
Frappe correctness, security, performance, concurrency, readability, API design, and test review. |
review-like-ankush |
Review of a Frappe diff or pull request. |
technical-writing |
Documentation, READMEs, commits, pull requests, and release notes. |
ui-design |
General dashboard design. Use the task-specific interface skills in agent tooling setup for copy, accessibility, layout, typography, and polish. |
grill-me |
Strict review of a plan or design before handover. |
diagnose |
A hard bug or a performance regression. |
draft-security-advisory |
A security advisory for a fixed vulnerability. |
i-have-adhd |
Focused, action-first output. Invoke it by name. |
Do not use frappe-app-dev in this repository.