Skip to content
prod e051e98
Browse

Architecture & systems

How the whole system fits together

You already have the building blocks — modules, customizations, schema extensions, the three-tier admin map. This guide steps back and shows the whole machine: how those pieces connect, what holds them together, and why the arrangement keeps a vendor app update-safe for years. Read it once to get the mental model, then drop into the handbook pages and playbooks where you actually build.

A CodeCanyon app is someone else’s codebase that you license and keep upgrading. The naive way to extend it — edit the vendor’s files in place — wins today and loses on every release: each update becomes a merge battle. The Zajaly architecture exists to make one promise true: the vendor can ship update after update while everything you built survives untouched. That promise only holds because of how the pieces fit, not any single trick.

Think of it like LEGO blocks. Each feature is a self-contained brick that snaps onto the app without gluing itself to the vendor’s code. Add a brick, remove a brick, or swap one for another — the structure never cracks.

Every pattern in this architecture is a different expression of one idea:

Listen, don't edit.
Extend, don't modify.
Isolate, don't scatter.
Toggle, don't redeploy.

If a change ever forces you to break one of those lines, that is the signal you reached for the wrong lane. The rest of this guide is just how the system makes each line cheap to follow.

Four principles carry the whole design. Internalize these and every specific rule downstream becomes obvious.

PillarWhat it meansWhat it buys you
Single-toggle controlOne flag turns any feature on or offEmergency kill switch in seconds, no deploy
Feature isolationEach feature is its own self-contained packageFind it, update it, delete it — all in one folder
Event-driven integrationFeatures listen to events; they never edit core codeVendor updates are a clean git pull, zero merges
Defensive codingNever assume a field exists or has the type you expectThird-party data quirks degrade gracefully, not crash

Two of those pillars carry the most weight, so they’re worth a closer look.

Laravel already fires events for nearly everything that happens in the app — a user registers, logs in, verifies email, resets a password. Instead of editing the vendor’s controller to bolt on your behavior, your feature subscribes to the event the app already emits and reacts to it. The vendor never knows your code exists.

graph TD
Vendor["Vendor app fires an event<br/>(e.g. User Registered)"]
Vendor --> A["Module A listens<br/>→ sync user to a CRM"]
Vendor --> B["Module B listens<br/>→ send to analytics"]
Vendor --> C["Module C listens<br/>→ write an audit log"]
A --> Done["Vendor update? Just pull.<br/>Nothing to merge."]
B --> Done
C --> Done

The arrows only ever point away from the vendor. Core code emits; your modules consume. Because nothing flows back into the vendor’s files, the next release drops in clean. That single property is what turns a 3-hour merge-conflict afternoon into a git pull.

The catch with integrating against someone else’s app: you can’t trust the shapes. A date field might be a plain string instead of a Carbon object; a column you expect might not exist; a relationship might be undefined. The discipline is three habits — never assume, always check, fall back safely — so one surprise from the vendor degrades quietly instead of taking the app down.

Here’s how a single module actually runs when an event fires — the five roles every Laravel feature plays, entry to action:

graph LR
SP["Service Provider<br/>(entry point: registers<br/>the module + listeners)"]
L["Listener<br/>(catches the event)"]
S["Service<br/>(your business logic<br/>+ API calls)"]
CFG["Config<br/>(flags + keys, read first)"]
SP --> L
L --> S
CFG -.->|"gates every step"| SP
CFG -.->|"gates every step"| L

Read it left to right: the service provider wires the module up at boot, a listener waits for the relevant event, and when it fires the service does the real work. The config sits underneath all of it — every layer checks the enabled flag first, so a single toggle silences the whole chain. (React and Node express the same five roles with hooks/middleware instead of providers/listeners — the discipline travels; only the syntax changes.)

The control model — six levels of “is this on?”

Section titled “The control model — six levels of “is this on?””

“Single-toggle control” is the headline, but in a real SaaS you need graduated control, not just one switch. The same feature can be gated at six levels, each answering a different question:

LevelWhere it’s decidedAnswers
Global.env flagIs this feature on at all?
FeatureConfig arrayWhich sub-features are on?
PlanMiddlewareDoes this subscription tier get it?
UserDatabase flagIs this specific user (beta) allowed?
AdminSuper-admin checkBypass — admins always get it
Kill switch.env flagEmergency off, no deploy

This is what lets one architecture serve a free-vs-pro pricing model, a gradual percentage rollout, and an instant incident response — all from the same module, no rewiring. The kill switch is the safety net that makes the whole thing fearless: discover a problem, flip one flag, and the feature stops processing immediately while you investigate.

The same “isolate, don’t scatter” rule applies to CSS and JS, not just PHP. Each feature keeps its stylesheet with the feature, and a single central loader pulls in only the enabled ones. Two ideas hold it together:

  • One loader line. Layouts include a single partial; the loader includes each module’s assets only when that module is enabled. Add a feature → no layout edits. Delete a feature → its CSS vanishes with the folder, no orphans left behind.
  • Central design tokens. Every module’s CSS references shared CSS-variable tokens (--zajmod-primary, --zajmod-bg, …) instead of hardcoding colors. Change a brand color in one tokens file and every feature updates — light and dark mode included, automatically.

The payoff mirrors the code side: adding a brick never forces you to touch the rest of the wall.

The architecture is a philosophy, not a framework — the principles hold in any stack; only the implementation changes. Knowing which is which keeps you from copying a Laravel detail into a React app where it doesn’t belong:

ConceptTravels everywhere?In Laravel it looks like…
Single-toggle controlYes.env flag + config check
Feature isolationYespackages/ZajModules/ folder
Event-driven integrationMostly (varies by stack)Event listeners
Defensive codingYes??, isset(), safe accessors
Compliance-ready designYesPer-module consent/audit config
Kill switch & rollbackYes.env flag + Deployer rollback
CSS token systemYesCentral tokens.css variables
Service Provider patternNo — LaravelReact uses hooks; Node uses middleware
Blade viewsNo — LaravelReact uses components

The takeaway: the discipline is portable; the paths and primitives are not. Build the next app in a different stack and you keep the four pillars, swap the syntax.

This guide is the map. Each system on it has its own home — a handbook chapter that goes deep on the why, or a playbook that walks the how. Use this as your jump table:

SystemLearn the detailExecute it
Module vs customization vs schemaModules vs customizationsCustomizations playbook
Where ops files liveThree-tier admin model
Data the vendor doesn’t storeSchema managementCustomizations playbook
Compliance, secrets, defense-in-depthSecurity & complianceSecurity & monitoring
Shipping a change safelyDeployment conceptsContinuous deploy
Surviving a vendor releaseVersion managementVendor updates