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:

TablePurpose
useruser_id, email, password, shard_id, role flags
deviceRegistered devices with browser, IP, last active time (session management)
api_keyRemember-me tokens and API keys
forgot / invitePassword reset and invitation tokens
auth_settingsRegistration mode, reCAPTCHA, branding
email_queue / email_settingsOutbound email queue and SMTP config
notification_categoryGlobal notification category definitions
consent_versionVersioned legal documents (ToS, Privacy Policy, etc.)
user_consentImmutable audit log of consent decisions
login_historyEvery login attempt with method, status, IP, browser
account_deletion_requestGDPR right-to-erasure requests with cooling-off
data_export_requestGDPR 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.

TablePurpose
user_profileName, avatar, homedir, referral code (keyed by user_id)
notificationPer-user notifications
notification_preferencePer-category delivery preferences
push_subscriptionWeb 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.

Each component has its own databases. Admin has a main DB + N shards. Each child app has its own separate main DB + N shards. The same 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

FileSession GuardUse 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.

Critical: Two-step process. Every migration requires both an SQL file AND a version bump in 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.