Skip to content

Architecture Overview

Frappe is a full-stack, metadata-driven web framework written in Python and JavaScript. Unlike traditional frameworks where you write models, views, routes, and controllers from scratch, Frappe generates most of that from declarative definitions called DocTypes. If you’re coming from Node.js, think of Frappe as an opinionated, batteries-included framework — roughly NestJS + Prisma + AdminJS — except everything is driven by metadata rather than decorators.

Frappe gives you:

  • An ORM with automatic database schema management
  • A REST and RPC API layer, auto-generated from DocTypes
  • A single-page admin interface called Desk
  • A website builder with Jinja2 templates
  • Background job processing via Redis Queue (RQ)
  • Role-based permissions, workflows, and print formats
  • Real-time updates via Socket.IO

ERPNext is a Frappe app — one of many that can be installed on a Frappe site. It isn’t a separate product; it’s a collection of DocTypes, controllers, reports, and print formats that implement accounting, inventory, manufacturing, HR, CRM, and more.

The Frappe stack
Rendering diagram…

This layering means:

  • Frappe can run without ERPNext (for building any business application).
  • ERPNext cannot run without Frappe (it depends on the framework).
  • Your custom apps sit on top and can extend or override both Frappe and ERPNext.

This is the single most important architectural concept to grasp. Frappe uses a three-level hierarchy:

  • Bench — a deployment directory containing the framework, apps, sites, and a shared Python virtualenv. One bench = one set of app versions.
  • App — a Python/JavaScript package that follows Frappe conventions. Frappe itself is an app; ERPNext is another; your custom code is yet another.
  • Site — a tenant with its own database, file storage, and configuration. Multiple sites can run on one bench, each with a different combination of installed apps.
  • Directoryfrappe-bench/ the “Bench”
    • Directoryapps/
      • Directoryfrappe/ the framework itself
      • Directoryerpnext/ the ERPNext modules
      • Directoryscoopjoy/ your custom app
    • Directorysites/
      • Directoryicecream.localhost/ Site 1
        • site_config.json
        • Directoryprivate/
        • Directorypublic/
      • Directoryfranchise-hq.localhost/ Site 2
        • site_config.json
      • common_site_config.json shared config for all sites
    • Directoryenv/ Python virtualenv
    • Directoryconfig/ Redis, Nginx, Supervisor configs
    • Procfile process definitions for bench start
    • patches.txt

Frappe v16 runs on the following stack:

ComponentVersionRole
Python3.14+Server-side logic, ORM, controllers, API
Node.js24 LTSAsset bundling (esbuild), Socket.IO server
MariaDB10.6+Primary relational database (Postgres 14+ also supported)
Redis8+Caching, job queues, real-time pub/sub
NginxlatestReverse proxy, static files, SSL termination
GunicornlatestWSGI application server (production)
WerkzeuglatestWSGI toolkit (development server)
Socket.IOlatestReal-time bidirectional communication
SupervisorlatestProcess management in production
wkhtmltopdf0.12.6+Legacy PDF generation (v16 adds Chrome-based PDF)

Understanding how a request flows through the stack is critical for debugging and performance tuning.

Request flow
Rendering diagram…

The handler dispatches on the URL pattern: /api/method/* hits a whitelisted Python function, /api/resource/* hits the auto-generated DocType REST API, /app/* serves the Desk SPA, and everything else renders a website page through Jinja. For real-time events (document updates, notifications), Frappe publishes to Redis and the Node.js Socket.IO server pushes updates to connected browsers.

  1. The browser sends POST /api/resource/Sales Order.
  2. Frappe authenticates and identifies the site from the Host header.
  3. It loads the Sales Order DocType definition (cached in Redis).
  4. before_validate() runs — your custom hook.
  5. Mandatory fields, links, and formulas are validated.
  6. validate() runs — business logic.
  7. before_save() runs.
  8. Rows are written to tabSales Order and tabSales Order Item in MariaDB.
  9. after_save() / on_update() run.
  10. The transaction commits.
  11. A real-time event is published via Redis → Socket.IO.
  12. A JSON response returns to the browser.

Long-running tasks don’t execute in the request-response cycle. Frappe uses Python RQ (Redis Queue) for asynchronous job processing.

Background job flow
Rendering diagram…

There are three default queues, each handled by a dedicated worker process:

  • default — standard background jobs (sending emails, generating reports).
  • short — quick tasks that should complete within seconds.
  • long — heavy tasks (data import, bulk operations, report generation).
# Enqueuing a background job from your app controller
import frappe
frappe.enqueue(
"scoopjoy.tasks.sync_franchise_inventory",
queue="long",
timeout=600,
franchise_id="FRAN-001",
)

The scheduler (managed by bench enable-scheduler) runs periodic tasks defined in your app’s hooks.py:

scoopjoy/hooks.py
scheduler_events = {
"daily": [
"scoopjoy.tasks.generate_daily_sales_report",
],
"hourly": [
"scoopjoy.tasks.sync_ingredient_stock",
],
"cron": {
"0 9 * * *": [
"scoopjoy.tasks.send_franchise_morning_briefing",
],
},
}

The “everything is a DocType” philosophy

Section titled “The “everything is a DocType” philosophy”

This is the concept Node.js developers often find most foreign — and most powerful once it clicks. In Frappe, every entity is a DocType: a Customer, a Sales Invoice, a User, an Email Account, a Print Format, a Workflow, a Report — even DocType itself — is defined as a DocType. That makes the system self-referential and metadata-driven:

What a DocType generates
Rendering diagram…
  1. Creating a DocType automatically creates a database table, a REST API, a form view, a list view, permission rules, search indexing, and change tracking.
  2. No migration files. When you modify a DocType, Frappe reads the JSON definition and alters the table automatically on bench migrate.
  3. Customization without code. Admins can add Custom Fields, set Property Setters, and create Client Scripts — all stored as DocTypes themselves.
TypeDescriptionExample
RegularStandard CRUD document with list + form viewsCustomer, Sales Order
Child TableRows inside a parent document (1:N)Sales Order Item
SingleSingleton — only one record exists (settings)Selling Settings
SubmittableHas a Draft → Submitted → Cancelled lifecycleSales Invoice, Journal Entry
TreeHierarchical parent-child structureAccount, Territory
VirtualNo database table; data comes from an external sourceCustom API-backed DocType

This single JSON file gives you a database table, REST API, admin form, list view, permissions, and search — with zero additional code.

scoopjoy/scoopjoy/doctype/ice_cream_flavor/ice_cream_flavor.json
{
"name": "Ice Cream Flavor",
"module": "ScoopJoy",
"doctype": "DocType",
"is_tree": 0,
"is_submittable": 0,
"fields": [
{ "fieldname": "flavor_name", "fieldtype": "Data", "label": "Flavor Name", "reqd": 1, "unique": 1 },
{ "fieldname": "base_type", "fieldtype": "Select", "label": "Base Type", "options": "\nDairy\nSorbet\nVegan", "default": "Dairy" },
{ "fieldname": "cost_per_scoop", "fieldtype": "Currency", "label": "Cost Per Scoop", "reqd": 1 },
{ "fieldname": "is_seasonal", "fieldtype": "Check", "label": "Is Seasonal" },
{ "fieldname": "available_from", "fieldtype": "Date", "label": "Available From", "depends_on": "eval:doc.is_seasonal" },
{ "fieldname": "available_until", "fieldtype": "Date", "label": "Available Until", "depends_on": "eval:doc.is_seasonal" }
],
"permissions": [
{ "role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1 }
],
"naming_rule": "By fieldname",
"autoname": "field:flavor_name",
"sort_field": "creation",
"sort_order": "DESC"
}

Knowing where things live saves hours when you’re spelunking through framework or ERPNext source.

  • Directoryfrappe-bench/
    • Directoryapps/
      • Directoryfrappe/ framework core
        • Directoryfrappe/
          • Directorycore/ core DocTypes (User, DocType, File…)
          • Directoryemail/ email send + receive
          • Directoryclient/ Desk SPA source (JS/CSS)
          • Directorypublic/ static assets served by Nginx
          • Directorytemplates/ Jinja2 website templates
          • Directorydatabase/ DB abstraction (MariaDB + Postgres)
          • handler.py request handler
          • app.py WSGI application entry point
          • hooks.py framework hooks definition
      • Directoryerpnext/ the ERPNext app
        • Directoryerpnext/
          • Directoryaccounts/ Chart of Accounts, GL Entry, invoices
          • Directorystock/ Warehouse, Stock Entry, inventory
          • Directoryselling/ Quotation, Sales Order
          • Directorybuying/ Purchase Order, Supplier
          • Directorymanufacturing/ BOM, Work Order, Production Plan
          • Directorysetup/ Company, naming series, defaults
          • hooks.py ERPNext hooks into Frappe
    • Directorysites/
      • common_site_config.json shared config (Redis URLs, etc.)
      • Directoryicecream.localhost/
        • site_config.json site-specific config (db name, etc.)
        • Directoryprivate/ private files (attachments, backups)
        • Directorypublic/ public files (website images, etc.)
    • Directoryconfig/
      • redis_cache.conf
      • redis_queue.conf
      • supervisor.conf production process manager config
      • nginx.conf generated Nginx config
    • Procfile development server process definitions

How ERPNext v16 differs from older versions

Section titled “How ERPNext v16 differs from older versions”

ERPNext v16 (released January 2026, built on Frappe v16) is a major leap:

Areav15 and earlierv16
PerformanceStandard caching”Frappe Caffeine” — ~2× faster page loads and reports
UI/UXFixed-width layoutRedesigned workspace, persistent sidebar, full-width adaptive layout
List viewsFixed columnsScrollable with unlimited columns, horizontal scroll
Child tablesFixed columnsScrollable with sticky columns
PDF generationwkhtmltopdf onlyChrome-based rendering (modern CSS including flexbox)
SecurityBasic field-level permissionsRole-based field masking (sensitive fields show xxxx)
Python3.10+3.14+ (required)
Node.js18+24 LTS (required)
DatabaseMariaDB/Postgres onlyAdds native SQLite for lightweight deployments
Developer toolsManual lintingAutomated linter, typed APIs, app dependency management
AccountingBasic statementsCustomizable statement templates, separate COGS/service accounting
ManufacturingStandard MRPStock reservation, landed cost on stock entries, better traceability

Putting every moving part together — the browser, the proxy, the two application servers, and the data tier:

ERPNext production architecture
Rendering diagram…

With the architecture in hand, the next chapter sets up a working development environment so you can start poking at all of this live.