BFF Pattern & API Contracts
Source:
engineering/anshin-orchestrate(GitLab ID: 80) βorchestrate-core/anshin_bff/, Rev 2026-02-01
anshin_bff is designed, built, and maintained exclusively by Anshin Engineering. It is not a third-party or upstream package β it is a first-class custom Frappe app in the Orchestrate platform. All feature additions, bug fixes, and architectural changes must go through the Anshin engineering review process.
This is a critical platform component: every frontend API call passes through it, it enforces the response envelope contract, and it bridges Frappe OAuth/session auth to the SaaS Guard tenant isolation layer.
What Is the BFF?β
The Backend for Frontend (BFF) is a thin Frappe app (anshin_bff) that sits between the Next.js frontend and the underlying Frappe/ERPNext framework. It provides:
- Stable API contract β standard
{ok, data, pagination, error, meta}envelope that insulates the frontend from internal DocType structure changes - Authentication bridge β translates Frappe session cookies and OAuth Bearer tokens into a consistent auth model
- Tenant context injection β every endpoint is decorated with SaaS Guard scope decorators to ensure per-tenant data isolation
- Audit and rate limiting β per-request audit log entries, request ID tracing, and rate limit enforcement
Architecture Positionβ
Next.js Frontend (React 19)
β
β HTTPS β session cookie + CSRF token (Frappe Desk users)
β OR Bearer token (OAuth2 SPA / PKCE flow)
βΌ
NGINX Ingress (10.10.98.40)
β
βΌ
Frappe/ERPNext gunicorn
β
βββ before_request chain (runs on every request):
β 1. anshin_bff.utils.request_middleware.before_request (rate limit, audit, request ID)
β 2. saas_guard.enforcement.hook_registry.before_request (tenant scope resolution β L1)
β
βΌ
anshin_bff whitelisted endpoint βββ /api/method/anshin_bff.<module>.<function>
β
β SaaS Guard L4 decorator (@with_company_scope / @platform_only)
β
βΌ
Frappe/ERPNext + saas_guard
β permission_query_conditions (L2 β SQL filter)
β has_permission (L3 β doc-level check)
βΌ
MariaDB 11.8
The frontend calls ONLY anshin_bff endpoints β never Frappe DocType REST endpoints directly. This is a hard architectural contract.
Security Architecture: BFF β SaaS Guard β Frappe OAuthβ
The three Anshin-maintained layers form an interlocking security posture. Understanding their interaction is critical for all future development.
Authentication Flow (Session Cookie / Frappe Desk)β
1. User POSTs to /api/method/login (Frappe native)
2. Frappe validates credentials β issues httpOnly `sid` session cookie
3. Browser includes `sid` cookie on all subsequent requests automatically
4. BFF `before_request` reads frappe.session.user from the cookie context
5. SaaS Guard `before_request` resolves tenant scope from frappe.session.user
6. BFF endpoint's @with_company_scope decorator re-validates and sets frappe.local
Write operations also require X-Frappe-CSRF-Token header β the CSRF token is
available at frappe.local.session.data.csrf_token after login.
Authentication Flow (OAuth2 PKCE / Next.js SPA)β
1. SPA redirects to /api/method/frappe.integrations.oauth2.authorize
with response_type=code, code_challenge (PKCE), client_id, redirect_uri
2. User authenticates β Frappe OAuth issues authorization code
3. SPA exchanges code at /api/method/frappe.integrations.oauth2.get_token
with code_verifier (PKCE) β receives access_token + refresh_token
4. SPA stores tokens in httpOnly cookies via Next.js API proxy routes
(tokens never exposed to browser JS)
5. BFF requests include: Authorization: Bearer <access_token>
6. Frappe validates the Bearer token β sets frappe.session.user
7. SaaS Guard resolves tenant scope as normal
The OAuth Client document name MUST equal the client_id value. If they differ,
validate_client_id() fails silently and all token validation breaks. See
Frappe v16 Standards.
The post_install_config.py script ensures this is always set correctly after deploy.
Why The BFF Must Not Call Raw Frappe DocType Endpointsβ
The Frappe REST API for DocTypes (e.g., /api/resource/Booking/...) bypasses the BFF's
response envelope, bypasses BFF-level audit logging, and β critically β bypasses the
SaaS Guard L2/L3 query condition hooks only if the DocType is accessed through the BFF
decorator chain. Direct DocType REST calls still invoke L2/L3 hooks, but without the
L4 decorator confirmation step and without audit logging.
The hard rule: frontend code uses /api/method/anshin_bff.* only. Never /api/resource/*.
Endpoint Namespaceβ
All BFF endpoints are whitelisted Frappe methods under the anshin_bff namespace:
/api/method/anshin_bff.<module>.<function>
Module Directoryβ
| Module Path | Domain |
|---|---|
anshin_bff.modules.core.auth | Authentication, session |
anshin_bff.modules.core.preferences | User preferences |
anshin_bff.modules.commerce.storefront | Product catalog, storefront |
anshin_bff.modules.commerce.cart | Shopping cart |
anshin_bff.modules.commerce.payment | Payment processing |
anshin_bff.modules.commerce.availability | Inventory availability |
anshin_bff.modules.commerce.voucher | Vouchers and discounts |
anshin_bff.modules.commerce.refund | Refund processing |
anshin_bff.modules.booking.costing | Booking costing engine |
anshin_bff.modules.anna.* | AI agent (chat, voice, classification, discovery) |
anshin_bff.modules.cms.* | CMS (pages, media, FAQs, testimonials) |
anshin_bff.modules.channels.* | OTA/Social channel distribution |
anshin_bff.modules.helpdesk.* | Support tickets, agents, SLAs |
anshin_bff.modules.insights.* | Dashboards, reports, queries |
anshin_bff.modules.orchestration.* | N8N workflows, definitions |
Response Envelopeβ
Every BFF endpoint returns one of three envelope shapes. The frontend must never parse raw Frappe responses β always expect these shapes.
Success Responseβ
{
"ok": true,
"data": { ... },
"meta": {
"request_id": "req_abc123456789",
"server_time": "2026-01-25T12:00:00Z",
"site": "core-dev.orchestrate.anshin.us",
"enabled_modules": ["commerce", "anna", "helpdesk"],
"api_version": "v1"
}
}
Error Responseβ
{
"ok": false,
"error": {
"code": "BOOKING_NOT_FOUND",
"message": "Booking not found",
"details": null
},
"meta": { ... }
}
Paginated Responseβ
{
"ok": true,
"data": [ ... ],
"pagination": {
"page": 2,
"page_size": 10,
"total_count": 45,
"total_pages": 5,
"has_next": true,
"has_previous": true
},
"meta": { ... }
}
Response Utilitiesβ
All BFF endpoint functions use helpers from anshin_bff.utils.response:
from anshin_bff.utils.response import success_response, error_response, paginated_response, handle_exception
@frappe.whitelist(methods=["GET"])
def get_booking(booking_id):
try:
doc = frappe.get_doc("Booking", booking_id)
return success_response(data={"booking": doc.as_dict()})
except Exception as e:
return handle_exception(e)
@frappe.whitelist(methods=["GET"])
def list_bookings(page=1, page_size=20):
try:
page = int(page)
page_size = int(page_size)
total = frappe.db.count("Booking", {"company": get_current_company()})
items = frappe.get_all("Booking", ...)
return paginated_response(data=items, page=page, page_size=page_size, total_count=total)
except Exception as e:
return handle_exception(e)
Exception Mappingβ
handle_exception() maps Frappe exceptions to standard HTTP status codes:
| Frappe Exception | Error Code | HTTP Status |
|---|---|---|
frappe.DoesNotExistError | NOT_FOUND | 404 |
frappe.PermissionError | PERMISSION_DENIED | 403 |
frappe.ValidationError | VALIDATION_ERROR | 422 |
| Any other exception | INTERNAL_ERROR | 500 |
SaaS Guard Decorator Contractβ
Every BFF endpoint that accesses tenant data must use the appropriate SaaS Guard decorator
from saas_guard.enforcement.decorators. These are the L4 enforcement layer.
Calling frappe.get_all("Booking", ...) without @with_company_scope means the
company filter is applied at L2 (query conditions) but not confirmed at L4.
L4 decorators are required for audit logging of access denials and for explicit
scope confirmation at the endpoint boundary.
Standard Tenant Endpointβ
from saas_guard.enforcement.decorators import with_company_scope
from saas_guard.enforcement.tenant_context import get_current_company
@frappe.whitelist(methods=["GET"])
@with_company_scope
def get_my_bookings():
"""Standard tenant endpoint β scope is resolved and confirmed before entry."""
company = get_current_company()
bookings = frappe.get_all("Booking", filters={"company": company}, ...)
return success_response(data={"bookings": bookings})
Platform Admin Endpointβ
from saas_guard.enforcement.decorators import platform_only
@frappe.whitelist(methods=["GET"])
@platform_only
def list_all_tenants():
"""Platform admins only. No company filter β cross-tenant operation."""
companies = frappe.get_all("Company", filters={"is_tenant": 1}, ...)
return success_response(data={"tenants": companies})
Support Session Requiredβ
from saas_guard.enforcement.decorators import support_session_required
@frappe.whitelist(methods=["POST"])
@support_session_required
def view_tenant_data_as_support():
"""Platform staff must have an active SaaS Support Session."""
company = get_current_company() # returns the session's target company
...
Feature Entitlement Gateβ
from saas_guard.enforcement.decorators import check_entitlement, with_company_scope
@frappe.whitelist(methods=["GET"])
@check_entitlement("advanced_analytics") # outer decorator β runs first
@with_company_scope # inner decorator β scope already set
def get_advanced_report():
"""Only tenants with 'advanced_analytics' entitlement can call this."""
company = get_current_company()
...
Request Middleware (BFF Layer)β
The BFF registers before_request and after_request Frappe hooks via anshin_bff.utils.request_middleware:
before_request = ["anshin_bff.utils.request_middleware.before_request"]
after_request = ["anshin_bff.utils.request_middleware.after_request"]
These hooks handle:
- Rate limiting (per-user and per-IP)
- Audit logging (writes to
BFF Audit LogDocType) - Request ID generation and tracing
X-Request-IDresponse header injection
Execution order matters: BFF before_request runs before SaaS Guard's before_request
(Frappe processes hooks in app install order). BFF's audit record is created first, then
SaaS Guard resolves and stamps the tenant scope. The SaaS Guard scope is available in
after_request for BFF to stamp the final audit entry.
Module Detectionβ
The meta.enabled_modules field in every response is populated by anshin_bff.utils.app_detector.get_enabled_modules(). This allows the frontend to conditionally render UI sections based on which optional Frappe apps are installed.
from anshin_bff.utils.app_detector import get_enabled_modules
# Returns list of enabled module names, e.g. ["commerce", "anna", "helpdesk"]
modules = get_enabled_modules()
Scheduled Tasksβ
scheduler_events = {
"cron": {
"* * * * *": [
"anshin_bff.modules.wordpress.reservations.expire_wordpress_holds"
],
},
"hourly": [
"anshin_bff.anshin_bff.doctype.dead_letter_queue.dead_letter_queue.process_pending_dlq"
],
"weekly": [
"anshin_bff.tasks.cleanup_old_audit_logs"
],
}
| Schedule | Task |
|---|---|
| Every minute | Expire draft bookings for WordPress holds that have timed out |
| Hourly | Process Dead Letter Queue β retry failed background jobs |
| Weekly | Clean up audit log entries older than 90 days |
DocTypes Owned by anshin_bffβ
| DocType | Purpose |
|---|---|
BFF Audit Log | Per-request audit trail (user, endpoint, timestamp, status, tenant scope) |
Dead Letter Queue | Failed background job retry queue |
Feature Flag | Per-tenant feature toggle management |
Tenant Integrationβ
BFF endpoints use get_current_company() from saas_guard to enforce per-tenant data isolation. The BFF does NOT implement tenant logic β it delegates entirely to saas_guard through the decorator chain:
from saas_guard.enforcement.decorators import with_company_scope
from saas_guard.enforcement.tenant_context import get_current_company
@frappe.whitelist(methods=["GET"])
@with_company_scope
def list_bookings():
company = get_current_company() # safe: @with_company_scope already validated this
bookings = frappe.get_all("Booking", filters={"company": company}, ...)
return success_response(data={"bookings": bookings})
See SaaS Guard β Multi-Tenant Architecture for the full 4-layer defense model.
Document Controlβ
| Rev | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-02-01 | Anshin Engineering | Initial release |
| 1.1 | 2026-03-22 | Anshin Engineering | Added BFFβSaaS GuardβOAuth security posture; corrected decorator names; added entitlement gating |