Skip to main content

BFF Pattern & API Contracts

Source: engineering/anshin-orchestrate (GitLab ID: 80) β€” orchestrate-core/anshin_bff/, Rev 2026-02-01

Anshin-Developed Application

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:

  1. Stable API contract β€” standard {ok, data, pagination, error, meta} envelope that insulates the frontend from internal DocType structure changes
  2. Authentication bridge β€” translates Frappe session cookies and OAuth Bearer tokens into a consistent auth model
  3. Tenant context injection β€” every endpoint is decorated with SaaS Guard scope decorators to ensure per-tenant data isolation
  4. 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.

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
OAuth Client Setup β€” Critical v16 Rule

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 PathDomain
anshin_bff.modules.core.authAuthentication, session
anshin_bff.modules.core.preferencesUser preferences
anshin_bff.modules.commerce.storefrontProduct catalog, storefront
anshin_bff.modules.commerce.cartShopping cart
anshin_bff.modules.commerce.paymentPayment processing
anshin_bff.modules.commerce.availabilityInventory availability
anshin_bff.modules.commerce.voucherVouchers and discounts
anshin_bff.modules.commerce.refundRefund processing
anshin_bff.modules.booking.costingBooking 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 ExceptionError CodeHTTP Status
frappe.DoesNotExistErrorNOT_FOUND404
frappe.PermissionErrorPERMISSION_DENIED403
frappe.ValidationErrorVALIDATION_ERROR422
Any other exceptionINTERNAL_ERROR500

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.

Never access tenant data without a SaaS Guard decorator

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 Log DocType)
  • Request ID generation and tracing
  • X-Request-ID response 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"
],
}
ScheduleTask
Every minuteExpire draft bookings for WordPress holds that have timed out
HourlyProcess Dead Letter Queue β€” retry failed background jobs
WeeklyClean up audit log entries older than 90 days

DocTypes Owned by anshin_bff​

DocTypePurpose
BFF Audit LogPer-request audit trail (user, endpoint, timestamp, status, tenant scope)
Dead Letter QueueFailed background job retry queue
Feature FlagPer-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​

RevDateAuthorDescription
1.02026-02-01Anshin EngineeringInitial release
1.12026-03-22Anshin EngineeringAdded BFF↔SaaS Guard↔OAuth security posture; corrected decorator names; added entitlement gating