Architecture
Wave Networks uses a shard-based architecture where authentication lives on a single main database and user data is distributed across shard databases for horizontal scaling.
Shard Routing
Main Database (Auth Only)
The main database (wncore_main) contains only authentication-related tables:
| Table | Purpose |
|---|---|
user | user_id, email, password, shard_id, role flags |
device | Registered devices with browser, IP, last active time (session management) |
api_key | Remember-me tokens and API keys |
forgot / invite | Password reset and invitation tokens |
auth_settings | Registration mode, reCAPTCHA, branding |
email_queue / email_settings | Outbound email queue and SMTP config |
notification_category | Global notification category definitions |
consent_version | Versioned legal documents (ToS, Privacy Policy, etc.) |
user_consent | Immutable audit log of consent decisions |
login_history | Every login attempt with method, status, IP, browser |
account_deletion_request | GDPR right-to-erasure requests with cooling-off |
data_export_request | GDPR data portability export requests |
Shard Databases (User Data)
Shard databases (wncore_shard_1, wncore_shard_2, ...wncore_shard_N) store per-user data. The number of shards is configurable during installation (1 to 20+). You can add more shards at any time by adding entries to $shardConfigs.
| Table | Purpose |
|---|---|
user_profile | Name, avatar, homedir, referral code (keyed by user_id) |
notification | Per-user notifications |
notification_preference | Per-category delivery preferences |
push_subscription | Web Push endpoint subscriptions |
Login Flow
1. User submits email + password
2. SELECT user_id, password, shard_id FROM user WHERE email = ?
3. Verify password with password_verify()
4. Store user_id + shard_id in $_SESSION
5. prime_shard($_SESSION['shard_id']) // opens shard DB connection
6. Load user_profile from the shard
7. All subsequent requests use session — zero extra lookups
New User Shard Assignment
When a user registers, they're assigned to the least-loaded shard:
SELECT shard_id, COUNT(*) as cnt FROM user
GROUP BY shard_id ORDER BY cnt ASC LIMIT 1
The shard_id never changes after registration. Adding new shards is as simple as adding entries to $shardConfigs.
shard_id from the admin user table routes across all apps — admin uses prime_shard(), child apps use child_prime_shard(), both keyed by $_SESSION['shard_id'].Bootstrap Chain
The bootstrap sequence when a child app page loads:
// 1. Child app's common.php includes admin's common.php
include(__DIR__ . '/../../admin/include/common.php');
// Admin's common.php does (in order):
// a. Composer autoload
// b. Load config (config.php or env vars)
// c. Open $db PDO connection to main DB
// d. Glob-include all helpers from include/common/
// e. Run admin migrations (check_and_migrate_main_db)
// f. Run shard migrations (check_and_migrate_all_shards)
// g. Start session + guard (redirect to login if not authenticated)
// h. Include definition.php
// i. Glob-include all action files
// 2. Child app then:
// a. Load its own config (child DB credentials)
// b. Open $child_db connection
// c. Run child app migrations
// d. Include child helpers and action files
Four Bootstrap Variants
| File | Session Guard | Use Case |
|---|---|---|
common.php |
Yes — redirects to login | Authenticated app pages (app/index.php) |
common_auth.php |
No | Login, register, forgot password pages |
common_api.php |
No (manual check) | AJAX API endpoint (api/index.php) |
common_readonly.php |
No | Cron jobs (CLI-only, no actions loaded) |
Config Loading
The system supports two config sources that use the same variables:
// common.php checks for config file first:
if (file_exists(__DIR__ . '/../config/config.php')) {
include($configFile); // Shared hosting: all vars in config.php
} else {
// Docker/CI: read from environment variables
$dbHostSpec = getenv('DB_HOST_MAIN') ?: 'localhost';
$dbInstance = getenv('DB_NAME_MAIN') ?: 'wncore_main';
// ... etc
}
config.php is gitignored. config.sample.php is committed as a reference template. The installer generates config.php automatically.
File Storage
User files are stored outside the webroot for security:
// $files_location is set in config (absolute path, trailing slash)
$files_location = '/home/username/files/';
// On registration, each user gets a bucketed home directory:
create_home_dir_id($user_id);
// Stores: /home/username/files/home/00/12/34/user_id/
// The path is saved in user_profile.homedir on the shard
// All subsequent access uses the stored path — no recomputation
Child apps can create namespaced subdirectories for their own file storage:
create_namespaced_dir($user_id, 'my-app');
// Creates: {homedir}/my-app/
Migration System
Migrations run automatically on every page load. The system compares version numbers and executes pending SQL files in order.
common.php. If you skip the version bump, the migration will never run.// Step 1: Create SQL file
// db_migrations/main/1.1.sql
// Step 2: Update version in include/common.php
$db_version = 1.1; // for main DB
$shard_version = 1.1; // for shard DBs
On each page load:
check_and_migrate_main_db()
→ reads $db_version from common.php
→ reads current version from db_version table in the database
→ if $db_version > current, runs pending SQL files in order
→ updates db_version table
check_and_migrate_all_shards()
→ same process for each shard in $shardConfigs
Version numbering is decimal: 1.0, 1.1, 1.2, 2.0. Main DB and shard DB versions are independent. Never edit existing migration files — always create a new version.