SEPO Engine / SepoDesk – Developer & Operations Guide
This document captures the architectural decisions, common pitfalls, and operational best practices for the SepoDesk WordPress plugin / standalone application engine. It is the reference for maintaining, extending, and troubleshooting the system.
1. System Overview
SepoDesk is a modular application engine built to run inside WordPress (or standalone). It provides:
- A role‑based JSON API (
/sepodesk/api/v2/data) with dynamic table and record operations. - Server‑rendered resources (admin panels, dashboards) via
ResourceController. - Client‑side apps that use the same API through
AppDataManagerand theEcmaScriptController. - Multi‑database support with logical handles (
sp_db_auth,sp_db_apps, etc.) that map to physical database names via environment variables. - Payment processing (mobile money, PawaPay) integrated with shop, booking, and LMS modules.
All connections are managed centrally to avoid duplication and ensure resilience.
2. Architecture Components
| Component | Purpose |
|---|---|
DbConnectionManager | Singleton that registers and caches all PDO connections. Handles retries with exponential backoff. Forced to use 127.0.0.1 (TCP) to avoid socket issues on shared hosting. |
SpEnvLoader | Reads .env and environment variables. Provides decryption for secrets (CryptoAES) and boolean parsing. |
PdoFactory | Low‑level PDO factory with connection pooling (static cache) and detailed error logging. Throws ConnectionFailedException or DatabaseNotFoundException. |
AppShell | Legacy wrapper that now delegates to DbConnectionManager. Used by CoreDataApiController to lazy‑load database connections for apps. |
ServerAppShell | Similar to AppShell, but for server‑rendered resources. Also delegates to DbConnectionManager. |
EcmaScriptController (JS) | Browser‑side API client with circuit breaker, request throttling, caching, and token refresh. |
ApiLogic (JS) | Wrapper around EcmaScriptController that adds bootstrapping (session hydration) and ready() promise. |
ApiKeyModel / ApiRoleResolver | Resolve an API key to a set of roles (stored as JSON in tsp_sys_api_keys). |
RoleCredentialsResolver | Maps a role to database credentials (host, user, password) from tsp_sys_core_roles_map. |
RoleTargetDbResolver | Resolves the target database for a given role and table using the access cache. |
3. Database Layer – Connection Management
3.1 DbConnectionManager (core)
- Lazy loads – connections are established only when first requested.
- Caches – once a PDO is created, it is reused for the duration of the request.
- Retry logic – up to 5 attempts with exponential backoff (100, 200, 400, 800, 1600 ms) for transient errors (codes
2002,2006,1040,1203and message patterns like “Operation not permitted”). - Forced host – always uses
127.0.0.1to avoid Unix socket path mismatches on shared hosting. - Environment‑driven – reads
POPULATOR_DB_{1..4}_HOST,_NAME,_USER,_PASS_ENCfrom.env. - Logging – detailed debug logs for every connection attempt, retry, and failure.
3.2 PDO Factory & Error Classification
PdoFactory::get()builds the DSN and callsnew PDO().- Throws
ConnectionFailedException(network / credentials) orDatabaseNotFoundException(unknown database). - The manager catches these and decides whether to retry based on the driver error code (
errorInfo[1]) and message content.
3.3 Connection Reuse Across Components
AppShellandServerAppShelldelegate toDbConnectionManager.DbConnectionSelectorTrait(used byPaymentNotifier, etc.) also calls the manager.- All components share the same static connection pool, eliminating duplicate connections.
4. Authentication & Authorisation
4.1 API Key → Roles
- Each API key stores a JSON array of roles (
sp_api_roles_name). ApiRoleResolverreads this JSON, caches the result (viaApiKeyRoleCache), and returns the array.- The connector (
SpEngineConnector) then iterates over the roles to find one that can resolve both a target database and credentials.
4.2 Role → Target Database
RoleTargetDbResolverusestsp_sys_role_db_access(populated by the installer) to map roles to databases.- Supports both explicit database override and table‑based routing (
sysTableortableText).
4.3 Role → Credentials
RoleCredentialsResolverreadstsp_sys_core_roles_mapfor the role’s host, user, and encrypted password.- The password is decrypted using
CryptoAESandAPP_SECRET_KEY.
4.4 Fallback & Error Handling
- If a role fails (no mapping or invalid credentials), the connector moves to the next role.
- If no role succeeds, a clear exception is thrown with context.
5. Frontend API Client (EcmaScriptController)
5.1 Circuit Breaker
- Automatically opens after repeated
5xxerrors or network failures. - Blocks all further requests for 30 seconds (for transient errors) or 5 minutes (for server errors).
- Auto‑resets after the timer expires.
- Auth requests bypass the breaker so token refresh can still happen.
5.2 Request Throttling
- Configurable
requestDelay(e.g., 200 ms) between consecutive requests. - Prevents bursts that could overload the server or hit connection limits.
- Debug logs show the delay being applied.
5.3 Retry & Idempotency
- Only idempotent actions (
select,fetchData,getSchema, etc.) are retried. - Mutations (
insertMultiple,updateMultiple,delete) are not retried to avoid duplicate rows. - Retries use exponential backoff (500, 1000, 2000 ms) up to
maxRetries(default 3).
5.4 Caching
- Response cache with TTL (default 30 seconds).
staleWhileRevalidate– serves stale data while refreshing in the background.- Cache stored in memory or sessionStorage (opt‑in).
5.5 Token Refresh
- Single‑flight (
_refreshPromise) so multiple 401s do not trigger parallel refreshes. - Refresh token is HttpOnly, never exposed to JS.
6. WordPress Integration
6.1 Plugin Bootstrap
- The plugin’s
index.phpchecks forABSPATHandWORDPRESS_MODEto determine runtime. - Autoloader (
Url::registerAutoloader()) is registered before any hooks, so all classes are available when hooks fire. - Safe mode (
SepoEngine\WordPress\SafeMode) prevents the engine from loading if a previous fatal error occurred.
6.2 Kernel & Routing
Kernelhandles bootstrapping (session, environment, service registration, routing).- For WordPress, the kernel is run via
KernelBridge(inwp:activateandtemplate_redirect). - Routes are defined in
Routes.phpandPagesRoutes.php;mountFilesserves documentation pages.
6.3 Case Sensitivity
- On Linux (Hostinger), folder and file names are case‑sensitive.
- All namespaces use PascalCase (e.g.,
SepoEngine\WordPress\Enqueue\AssetsManager). Folders must match exactly. - The installer and autoloader are case‑insensitive in their maps to avoid fatal errors.
7. Installation & Migrations
7.1 Installer (Installer/run.php)
- Explicitly loads only required leaf classes (no full framework).
- Checks
IS_PRODUCTION,INSTALLER_ENABLED,INSTALLER_KEY, and IP allow‑list. - Creates databases (if not SQLite) and tables from
Migration/Versionsfolder. - Runs data hydration from
Migration/Data.
7.2 Populators
RoleDbAccessPopulatorControllerbuilds thetsp_sys_role_db_accesstable.SchemaPopulatorControllerdeploys table schemas from blueprints.MigrationPopulatorControllerapplies versioned migrations.
7.3 SQLite Support
- Enabled via
SQLITE_ENABLED=true. - Connections use
.sqlitefiles instorage/database/. - The installer skips database creation and user management in SQLite mode.
8. Logging & Debugging
8.1 Logger (SepoEngine\Helpers\Logger)
- Writes to
Storage/Logs/sepo-YYYY-MM-DD.log. - Rotates files at 5 MB, retains 14 days.
- Redacts sensitive keys (
password,secret,token, etc.) from context. - Supports levels:
debug,info,warning,error,critical.
8.2 Debug Mode
- Set
APP_DEBUG=truein.envto see detailed error messages in JSON responses and stack traces. - Use
IS_PRODUCTION=trueto suppress user‑facing error details.
8.3 Monitoring
- All connections, retries, and failures are logged with full context.
- Request metrics (time, memory, URI) are logged in debug mode.
9. Common Issues & Solutions
9.1 SQLSTATE[HY000] [2002] Operation not permitted
- Cause: MySQL connection refused – usually because the host is
localhost(socket mismatch) or server overloaded. - Fix: Force
127.0.0.1(already done inDbConnectionManager). Enable retry logic (now with 5 attempts). Increasemax_connectionsif needed.
9.2 Class "SepoEngine\WordPress\Enqueue\AssetsManager" not found
- Cause: Folder name mismatch (case‑sensitive).
wordpressvsWordPressorenqueuevsEnqueue. - Fix: Rename folders to match namespace exactly. Use the installer’s case‑insensitive map as a fallback.
9.3 Circuit breaker opens (frontend requests blocked)
- Cause: Server returning 500s (often due to connection timeouts or missing tables).
- Fix: Fix the underlying 500 (check PHP logs). The breaker auto‑resets after 30 s (for 401) or 300 s (for 500). You can also call
engine.resetCircuit()in JS.
9.4 Role‑credential mismatch (wrong user for database)
- Cause:
tsp_sys_role_db_accessmaps a role to a database, buttsp_sys_core_roles_maphas credentials for a different database. - Fix: Ensure each role has a matching entry in both tables. The populator now assigns roles only to their own databases.
9.5 Token refresh deadlock on 401
- Fixed:
_execute()is decoupled from_request(); the refresh promise is single‑flighted. Auth requests bypass the circuit breaker.
10. Development & Production Best Practices
10.1 Environment Variables
- Use
IS_PRODUCTION=trueon live servers to hide debug info. - Set
SQLITE_ENABLED=falsefor MySQL;truefor SQLite. - Use
ALLOW_PLAINTEXT_SECRETS=trueonly during migration; turn off after encryption.
10.2 Database Credentials
- Encrypt passwords with
CryptoAES::encrypt()and store the ciphertext in.env. - Use
APP_SECRET_KEY(64 hex chars) as the master key.
10.3 Frontend Configuration
requestDelayshould be at least 150 ms to avoid overloading the server.concurrencyshould be low (2–3) on shared hosting.- Cache TTL can be increased for read‑heavy resources.
10.4 Deployment Checklist
- Delete
installer/folder after installation. - Set
INSTALLER_ENABLED=false. - Ensure
.envis not web‑accessible (.htaccessdenies it). - Run
DbConnectionManager::reset()after changing credentials? (Not needed – restart PHP). - Clear opcache and restart PHP after code updates.
10.5 Monitoring
- Enable
APP_DEBUG=truetemporarily to diagnose issues. - Monitor
Storage/Logs/sepo-*.logfor errors. - Use
SHOW PROCESSLISTto check for connection leaks.
11. Troubleshooting Guide (Quick Reference)
| Symptom | Likely Cause | Action |
|---|---|---|
500 on /api/v2/data | PDO connection failure | Check error_log; verify DbConnectionManager retry logs. |
Operation not permitted | Host misconfigured or socket issue | Force 127.0.0.1 (already done). |
| Frontend stuck with “Circuit open” | Repeated 500s | Fix server error; call engine.resetCircuit() in dev. |
Class not found in WordPress | Folder case mismatch | Rename folders to match namespace. |
| No roles resolved | API key missing or invalid | Check tsp_sys_api_keys; verify sp_api_roles_name is valid JSON. |
| Credentials mismatch | Role mapped to wrong DB | Re‑run RoleDbAccessPopulator with truncate=true. |
| Token refresh fails | Refresh cookie missing or expired | Log in again; check authUrl configuration. |
| Request throttling not working | requestDelay not set or debug off | Add requestDelay: 200 and debug: true to config. |
12. Useful Code Snippets
12.1 Manually test a database connection
$pdo = DbConnectionManager::ensure('sp_db_auth');
var_dump($pdo->query('SELECT 1')->fetch());