π Custom Development Lifecycle
Parts 2 and 3 of this series walked through the hospital's Clinical Management and Laboratory modules and the real estate firm's Property and Construction modules from the business side β what each screen means and why it exists. This part walks through the same four custom modules from the developer side: how a requirement actually becomes a DocType, a controller, a UI, an API, and eventually a deployed, monitored production system. This is the same custom-development lifecycle that Building a Custom Frappe App in the 9-part course covers generically β this part applies it concretely to the two real projects instead of a single teaching example.
Every DocType covered in this part β Patient, Lab Request, Property Unit, Property Booking, and the ones behind them from Parts 2 and 3 β passed through this exact chain. Skipping a step (most commonly permission design or testing) is where custom Frappe apps tend to accumulate the kind of technical debt that only surfaces once real users hit the system in production.
π¦ Custom App Structure for Both Projects
Both custom domains β hospital and real estate β can share one physical app layout, split into two module folders. The base directory pattern is the same one covered in the 9-part course's custom app structure section; the only difference here is that this app carries two unrelated business domains side by side instead of one:
custom_app/
βββ hooks.py
βββ modules.txt
βββ patches.txt
βββ public/
βββ templates/
βββ api/
βββ config/
βββ custom_app/
βββ hospital/
β βββ doctype/
β βββ report/
β βββ page/
β βββ workspace/
βββ property/
βββ doctype/
βββ report/
βββ page/
βββ workspace/
| Entry | What it holds |
|---|---|
hooks.py | App-level config: doc events, scheduler events, fixtures, includes β see Hooks & Events Deep Dive |
modules.txt | The modules this app declares (here, effectively Hospital and Property) |
patches.txt | Ordered list of data/schema migration scripts run on bench migrate |
public/ | JS/CSS/images served to the browser for both modules |
templates/ | Jinja templates for web pages, print formats, and emails |
api/ | Whitelisted Python methods exposed as custom REST-style endpoints (see Custom Module API Examples below) |
config/ | Module/desktop config β icons, labels, and workspace registration |
One app with two module folders and two separate apps are both legitimate choices, and the right one is a deployment decision more than a technical one:
| Choice | Tradeoff |
|---|---|
| One app, two modules | Simpler deployment and versioning β one bench get-app, one install, one version tag for both businesses. Harder to release a hospital-only fix without touching the property code path. |
| Two separate apps | Independent release cycles per business β the hospital app can ship a hotfix without redeploying the property app. More moving parts: two repos, two version histories, two install steps per site. |
π§± Four Worked DocType Designs
Parts 2 and 3 already explained what these DocTypes mean to the business β see Clinical Management and Laboratory for the hospital, and Property/Sales and Construction Management for real estate. This section covers the same four DocTypes from the developer view: how they'd typically be shaped as DocType definitions, illustratively β exact field names and behavior always depend on the actual implementation and version.
Patient (Hospital)
| Field | Type | Notes |
|---|---|---|
| naming | autoname | Illustrative naming series, e.g. PAT-.YYYY.-.##### |
| patient_name | Data | Mandatory |
| customer | Link (Customer) | Ties the Patient back to ERPNext core billing |
| primary_doctor | Link (Employee/Practitioner) | Optional at registration, set before a visit |
| permissions | β | Front-desk/registration roles: read+write; Doctor/Nurse roles: read+write on assigned patients; Admin: full |
| submit/cancel | β | Typically a simple, non-submittable master β a patient record persists and is edited, not submitted per visit |
Lab Request (Hospital)
| Field | Type | Notes |
|---|---|---|
| naming | autoname | Illustrative series, e.g. LAB-REQ-.YYYY.-.##### |
| patient | Link (Patient) | Mandatory |
| requested_tests | Table (child) | One row per requested test, mandatory at least one row |
| requesting_doctor | Link (Employee) | Set on creation |
| permissions | β | Doctor: create/read; Lab Technician: read/write on assigned requests; Lab Manager: submit/cancel |
| submit/cancel | β | Submittable transaction document β a submitted request is what a lab technician acts on, and cancellation follows the standard submit/cancel lifecycle |
Property Unit (Real Estate)
| Field | Type | Notes |
|---|---|---|
| naming | autoname | Illustrative, e.g. <project>-<floor>-<unit_no> |
| project | Link (Project) | Mandatory |
| unit_status | Select | e.g. Available / Booked / Sold β mandatory, defaults to Available |
| size_sqft | Float | Used in pricing calculations |
| permissions | β | Sales role: read; Property Manager role: read/write; Admin: full |
| submit/cancel | β | Simple master with a status field β not submittable itself; its status changes as bookings against it are submitted or cancelled |
Property Booking (Real Estate)
| Field | Type | Notes |
|---|---|---|
| naming | autoname | Illustrative series, e.g. BOOK-.YYYY.-.##### |
| unit | Link (Property Unit) | Mandatory |
| customer | Link (Customer) | Mandatory |
| installment_plan | Table (child) | Installment schedule rows |
| permissions | β | Sales Executive: create; Sales Manager: submit/cancel; Accounts: read for reconciliation |
| submit/cancel | β | Submittable transaction document β submitting is what should flip the linked Unit's status to Booked/Sold via server-side logic |
π Python Business Logic
Doc-event hooks are where server-side rules actually live, because β unlike client scripts β they cannot be bypassed by a browser tab or a direct API call:
| Hook | When it fires | Typical use here |
|---|---|---|
validate | Before every save, draft or submit | Property Booking: check the linked Unit's status is "Available" before allowing the booking to proceed |
before_save | Just before the record is written | Normalize/derive a computed field, e.g. total installment amount |
before_submit | Just before a submittable doc is submitted | Lab Request: confirm all requested tests have a result entered |
on_submit | Immediately after submit | Lab Report: auto-create a Sales Invoice for the linked Patient/Customer |
on_cancel | Immediately after cancel | Property Booking: revert the linked Unit's status back to Available |
on_update | After any save (draft or submitted) | Push a real-time notification to a dashboard (see Real-Time / WebSocket) |
autoname | While generating the document's name | Custom naming series per hospital department or per real estate project |
permissions | On every access check | Row-level rule, e.g. a Doctor only sees Lab Requests they raised, via permission_query_conditions |
The following is illustrative syntax only β a shape a validate() method for a Lab-Request-style controller might take, not exact code from a real deployment:
# Illustrative syntax β not guaranteed exact for any specific version
class LabRequest(Document):
def validate(self):
# validate patient
# validate requested tests
# validate status
Not every piece of logic belongs in the same place β picking the right layer matters as much as writing correct logic:
| Layer | Belongs here |
|---|---|
| Client Script | UI-only behavior: show/hide fields, set filters, prompt the user β see Client Script |
| Python controller | Rules that must always hold true regardless of how the document was created (form, API, import) |
Hook (hooks.py) | Cross-cutting logic tied to lifecycle events, especially logic reacting to another app's/module's DocType |
| API (whitelisted method) | Logic exposed deliberately to external callers, with its own auth/validation |
| Server Script | Admin-configurable logic without a full app deployment β see Server Script |
π±οΈ JavaScript / Client-Side
Client scripts shape what the user experiences in the moment: which fields appear, what a Link field's query is filtered to, what gets fetched from a related document, and what inline validation warns about before a save is even attempted. Typical client-side responsibilities include form events (refresh, onload, field-level triggers), dynamic field visibility, Link field queries scoped to relevant records, pulling and displaying data from a linked document, and lightweight UX-level validation.
The following is illustrative syntax only:
// Illustrative syntax β not guaranteed exact for any specific version
frappe.ui.form.on("Lab Request", {
refresh(frm) {
// UI behavior
}
});
π Security Architecture
For the full depth on roles and permissions, see Users, Roles & Permissions and the hardening checklist in Security Hardening. Every layer below matters β skipping any one of them is where real incidents tend to start:
| Concern | One-line explanation |
|---|---|
| Password security | Enforce a real password policy; never store or log plaintext passwords |
| HTTPS | Every request, every environment β including internal staging β should be TLS-encrypted |
| RBAC | Roles gate what a user can do; keep roles scoped to job function, not blanket admin access |
| API authentication | Every API caller must authenticate; no anonymous write access to business data |
| API key/secret | Scoped per integration, rotated on suspicion of exposure, never committed to source control |
| OAuth | Preferred for third-party/user-delegated integrations over shared static keys where supported |
| CSRF considerations | Browser-session requests need CSRF protection; token-authenticated API calls follow a different threat model |
| Server-side validation | The only validation that can't be bypassed by the caller β see JavaScript / Client-Side |
| Audit trail | Owner, modified_by, and version history on every document β essential for both clinical and financial records |
| File permissions | Private files (e.g. lab results, contracts) must not be served from a publicly readable path |
| Database credentials | Unique per environment, never reused between staging and production |
| Secrets management | Environment variables or a secrets manager β never hard-coded in app source |
| Firewall | Only the ports actually needed (typically 80/443/22) exposed to the internet |
| Least privilege | Every user, role, and service account gets the minimum access that lets it do its job |
π§΅ Redis in This Architecture
Redis is not optional infrastructure sitting off to the side β several core pieces of the stack depend on it directly:
Web
Queue
Scheduler
Socket
Redis
Redis backs the background job queue, response/data caching, and the real-time pub/sub channel that Socket.IO uses. If Redis becomes unavailable, the general expectation β not a guarantee for any specific version or deployment β is that background jobs (the hospital's nightly reminder emails, a bulk installment reminder job for the real estate firm) stall or stop being picked up, and real-time notifications (like a lab-result approval alert) stop delivering. Whether core document create/read/update/delete through the web process keeps working during a Redis outage depends on how much of that path is cached versus computed live, and should be verified against the actual deployment rather than assumed.
π‘ Real-Time / WebSocket
Two illustrative scenarios from the two custom apps:
- Hospital: a Lab Technician approves a Lab Result β a realtime event fires β the requesting doctor's dashboard shows a live notification, without a page refresh.
- Real estate: a Sales Manager approves a Property Booking β a realtime event fires β the sales dashboard updates unit availability for every sales executive currently viewing it.
ποΈ Database Architecture for Production
MariaDB is the primary, long-supported database engine for ERPNext and the Frappe Framework, and it's the safe default assumption for any production deployment in this series. PostgreSQL support has existed in parts of the Frappe ecosystem at various points, but its availability, maturity, and exact capabilities are version and deployment dependent β verify PostgreSQL support and its exact capabilities for the specific ERPNext/Frappe release before choosing it, and do not assume feature parity with MariaDB. This series does not instruct switching database engines and treats MariaDB as the baseline throughout.
| Concern | Conceptual note |
|---|---|
| Transactions | Multi-statement operations (e.g. a submit that writes several tables) should be atomic β all succeed or all roll back |
| Indexes | Fields used heavily in filters/joins (Link fields, status fields) benefit from indexing; over-indexing has its own write-performance cost |
| Backup | Regular, automated, verified β see Backup Architecture |
| Restore | A backup that has never been restored in a test is not a proven backup |
| Performance | Query patterns matter more than raw hardware once data volume grows β see Performance & Scalability |
| Connection management | Connection pooling and sane timeouts prevent one runaway process from starving the database |
| Migration | Schema changes should be applied through the framework's migration mechanism, not hand-run SQL β see Database Migration |
π Nginx in Production
| Concern | Role |
|---|---|
| Reverse proxy | Sits in front of the app server, forwarding requests to Gunicorn |
| SSL termination | Handles TLS so the app server itself doesn't manage certificates |
| HTTPβHTTPS redirect | Ensures no request is ever served unencrypted |
| Domain routing | Routes multiple site domains on one bench to the correct site |
| Static file serving | Serves JS/CSS/uploaded files directly, without hitting the Python process |
| Proxy headers | Forwards real client IP and protocol info to the app server |
| WebSocket support | Upgrade headers configured so Socket.IO connections work through the proxy |
| Timeout configuration | Long-running requests (large reports, bulk operations) need sane, explicit timeouts |
| Security headers | HSTS, X-Frame-Options, and similar headers reduce common web attack surface |
π Full Production Deployment Architecture
Internet
NGINX
Web App
WebSocket
Frappe
Workers
Scheduler
API
Redis
Database
Backups
Every box in this diagram appeared individually earlier in the series β the Beginner course's production diagram covers the same shape at a general level; this version reflects the full path both the hospital and real estate deployments run in practice.
π CI/CD Pipeline
- Git as the single source of truth for both custom apps' code
- Feature branches per requirement, never committing straight to the production branch
- Pull requests as the review gate before merge
- Version tags marking every release that reaches production
- Release notes summarizing what changed, for both the technical team and the business stakeholders
- Database migrations tested in staging before they ever run against production data
benchcommands (build, migrate, restart) scripted rather than run ad hoc by hand- A documented rollback strategy for every release, decided before deployment, not improvised after
π·οΈ Release & Versioning
| Version | Meaning |
|---|---|
1.0.0 | Initial release |
1.1.0 | New feature, backward compatible |
1.1.1 | Bug fix only, no new behavior |
A breaking change (removing a field the API relies on, changing a DocType's meaning) bumps the major version; a new, backward-compatible capability (a new report, a new optional field) bumps the minor version; a fix with no behavior change bumps the patch version. Treating these three categories differently is what lets other systems (the hospital's mobile app, the property website) know whether upgrading is safe without reading every commit.
ποΈ Database Migration
bench migrateas the standard mechanism for applying pending schema/data changes- Patches for one-off data transformations that a plain schema change can't express
- Schema changes (new fields, changed field types) driven through the DocType definition, not hand-written SQL
- Fixtures for exporting/reapplying configuration (custom fields, roles, workflows) across environments
- Data migration scripts tested against a copy of production-shaped data, not just an empty dev site
- Rollback planning decided before running a migration in production, not after something breaks
See Building a Custom Frappe App for how patches and fixtures are structured, and Backup & Recovery for why a backup always precedes a production migration.
πΎ Backup Architecture
Production ERPNext
Database Backup
Private Files
Public Files
Backup Storage
Daily
Weekly
Monthly
- Full database backup on a regular, automated schedule
- File backup covering both private files (lab results, contracts) and public files
- Encryption at rest for backup storage, especially given the clinical and financial data involved
- Offsite backup β never stored only on the same server it protects against losing
- A defined retention policy across daily/weekly/monthly tiers
- Restore testing on a real cadence, not a one-time exercise
- A documented disaster recovery plan (see Disaster Recovery)
- An explicit RPO (Recovery Point Objective) β how much data loss is acceptable
- An explicit RTO (Recovery Time Objective) β how long recovery is allowed to take
π Monitoring
| Category | Example signals |
|---|---|
| Infrastructure | CPU, RAM, disk usage and headroom |
| Application | Error rates, response time, application logs |
| Database | Slow queries, connection counts |
| Queue | Background job backlog, failed job count |
| Security | Failed login attempts, unusual API call volume |
| Business KPIs | Daily revenue, bookings, admissions β the same figures behind the hospital and real estate dashboards covered in Parts 2 and 3 |
π Reporting Architecture
| Type | One line |
|---|---|
| Query Report | SQL-defined tabular report, fast to build for a fixed question |
| Script Report | Python-defined report for logic too complex for a single query |
| Report Builder | No-code filtered/grouped view built entirely in the UI |
| Dashboard | A composed page of charts and number cards around one theme |
| Chart | A single visual aggregation, reusable across dashboards |
| Number Card | One headline metric, e.g. today's admissions or today's bookings |
π API Integration for Both Projects
- Authentication via API key/secret or OAuth, never anonymous write access
- GET for reads, POST for creates, PUT for updates, DELETE for removals β standard REST semantics
- Webhooks for outbound event notification (e.g. notifying the property website when a unit's status changes)
- Explicit error handling and meaningful HTTP status codes on every endpoint
- Rate limiting to protect the server from a runaway or misbehaving integration
- Logging every external API call for auditability
- Security scoped per integration β see REST API Deep Dive for the full mechanics
π External Integration
| Category | Meaning |
|---|---|
| Native | Built into ERPNext/Frappe already β no custom code needed |
| Custom | Built specifically for this deployment via the custom app's hooks and APIs |
| Third-party | An external service or system this deployment connects to over an API/webhook |
Illustrative examples per project, not a confirmed integration list for any real deployment:
- Hospital: SMS gateway, email, payment gateway, the hospital's public website, a mobile app, laboratory instruments (where technically supported by the specific hardware/interface), and accounting/reporting systems
- Real estate: the public marketing website, lead capture forms, payment gateway, SMS, email, maps, a customer self-service portal, and document/e-signature systems
π§© Custom Module API Examples
POST /api/method/hospital.api.create_lab_request
GET /api/method/hospital.api.get_patient
GET /api/method/hospital.api.get_lab_result
GET /api/method/property.api.available_units
POST /api/method/property.api.book_unit
GET /api/method/property.api.installments
These are illustrative example endpoint names only; actual endpoint design, naming, and security depend on the real implementation and must be reviewed against the target version's whitelisted-method conventions.
π§ͺ Testing Strategy
| Project | Example test case |
|---|---|
| Hospital | Submitting a Lab Report with an unapproved Result should be blocked |
| Real Estate | Booking a Unit already marked Sold should be blocked |
β Deployment Checklist
- Code reviewed
- Tests passed
- Database backup taken
- File backup taken
- Migration tested in staging
- Environment variables verified
- SSL verified
- Worker processes verified
- Scheduler verified
- Redis verified
- Database verified
- Nginx verified
- Login test
- Permission test
- API test
- Workflow test
- Accounting test
- Stock test
- Backup test
- Monitoring test
π Disaster Recovery
- RPO (Recovery Point Objective) β how much data the business can tolerate losing
- RTO (Recovery Time Objective) β how long the business can tolerate being down
- Backup retention long enough to cover the realistic detection window for a problem
- Restore testing done on a schedule, so the first real restore isn't the first attempted one
π₯ Business Continuity
Hospital: if ERPNext becomes unavailable during an emergency admission, the fallback is a paper-based manual process β a physical admission form, a manually logged Lab Request slip β that keeps patient care moving without waiting on the system. Once ERPNext is restored, that paper trail is entered back in as a reconciliation step, cross-checked against what actually happened during the outage before anything is billed or closed out.
Real estate: if ERPNext becomes unavailable while a buyer is ready to book a unit, the fallback is a temporary manual capture of the transaction (a paper or offline booking form) rather than losing the sale. Once the system is back, that capture is reconciled into a proper Property Booking record β with an explicit check against double-booking, since a unit could otherwise be manually promised to two buyers during the same outage window.
β‘ Performance & Scalability
- Database indexes on frequently filtered/joined fields
- Query optimization before reaching for more hardware
- Redis caching for expensive, frequently repeated reads
- Background jobs for anything that doesn't need to block the user's request
- Queue workers scaled to match actual job volume
- Pagination on every list/report that can return large result sets
- Reports designed with large datasets in mind from the start, not retrofitted after they get slow
- File storage that scales independently of the database
- API responses optimized to return only what the caller needs
The hospital's Lab Result history can realistically grow into millions of rows over years of operation, and the real estate firm's installment records can reach thousands of customers with recurring schedules β both are the kind of growth that makes indexing and query design matter more than raw server size.
π’ Multi-Company & Multi-Branch
| Hospital | Configuration option |
|---|---|
| Main Hospital / Diagnostic Center / Pharmacy | Either as separate Companies, or as one Company using Cost Centers to separate them β both are valid choices depending on how independently each unit needs to report financially |
| Real Estate | Configuration option |
|---|---|
| Development Company / Construction Company / Property Management Company | Same choice applies β separate Companies for independent books, or one Company with Cost Centers for a simpler consolidated view |
Branch examples: the hospital operating Dhaka Main Branch, Chittagong Branch, and Sylhet Branch; the real estate firm operating a Dhaka Office, a Chattogram Office, and individual Project Site Offices. Branch architecture isn't cosmetic β it affects how Warehouses, Cost Centers, and User/permission scoping (see User Types) are structured across the whole deployment.
βοΈ Hospital vs Real Estate: The Complete Comparison
| Area | Hospital | Real Estate |
|---|---|---|
| Customer | Patient / Corporate account | Buyer |
| Item | Medicine / Service | Material / Service |
| Project | Hospital Expansion | Construction Project |
| Stock | Medicine | Construction Material |
| Custom Module | Clinical Management | Property Management |
| Billing | Patient Billing | Apartment Sales |
| Workflow | Lab Approval | Booking Approval |
| Accounting | Healthcare Revenue | Property Revenue |
| HR | Doctor / Nurse | Engineer / Worker |
| Asset | Medical Equipment | Construction Equipment |
The shape is identical in both columns β only the vocabulary changes. That's the core lesson of the whole series: ERPNext's core modules (Accounting, Stock, HR, Projects) don't change between industries; only the custom module vocabulary layered on top of them does.
π§ Final Architecture: One Diagram for the Whole Series
BUSINESS
ERPNext PLATFORM
Hospital Custom App
Real Estate Custom App
Other Apps
ERPNext CORE
Accounting
Stock
HR
Database
Redis
Workers
Files
Nginx
Internet
Everything in this series β the methodology in Part 1, the two custom apps in Parts 2 and 3, and every mechanism in this part β is one instance of this one diagram.
πΊοΈ Developer Roadmap
π§© The Decision Framework, Applied
Every custom DocType and workflow built across both projects in this series β Patient, Lab Request, Property Unit, Property Booking, and everything alongside them β was arrived at by walking Part 1's Configure vs Customize vs Custom Module vs Integration decision framework: does an existing ERPNext feature already cover this? Configure it. Can a field or workflow addition cover it? Customize it. Does it need a genuinely new business object ERPNext has no concept of? Build a custom module. Does it need to talk to something outside ERPNext entirely? Build an integration. Two real, fully worked projects later, the framework held up in every case β nothing in this series needed to bypass it or modify ERPNext core.
π Final Project Assignment
Project A: Hospital Management ERP
Minimum requirements: Patient, Doctor, Appointment, Admission, Lab, Pharmacy, Billing, Accounting, Inventory, HR, Payroll, Reports.
Custom Apps: Hospital Clinical Management, Laboratory Management.
Project B: Real Estate & Construction ERP
Minimum requirements: Lead, Customer, Project, Building, Unit, Booking, Installment, Construction, BOQ, Procurement, Stock, Accounting, HR, Payroll, Reports.
Custom Apps: Property Management, Construction Management.
Both projects follow the same deliverable chain, start to finish:
π Series Complete
This closes the 4-part ERPNext Bangladesh Implementation Case Study: Part 1 β Implementation Methodology, Part 2 β Hospital Management, Part 3 β Real Estate & Construction, and this part, Part 4 β Custom Development, DevOps & Production.
Taken together with the existing 9-part ERPNext Beginner to Intermediate course and the two framework-agnostic accounting-theory posts β Invoice vs Payment: The Complete ERP Accounting Architecture and the Accounting to ERP curriculum β this site now carries one coherent body of ERPNext knowledge: the general architecture and course material, the accounting theory underneath it, and this case study series showing that architecture and theory applied to two real, fully worked Bangladesh businesses. Any one piece can be read on its own; read together, they form a complete path from "what is ERPNext" to "how do you actually build and run a production ERPNext deployment for a real business."