{
  "name": "Sub Theme",
  "version": "1.0",
  "description": "Self-hosted application platform with authentication, horizontal database sharding, email queues, push notifications, and a child app pattern. Runs on commodity shared hosting.",
  "documentation": "https://subtheme.com/docs",

  "repositories": {
    "core": {
      "url": "https://github.com/WaveNetworks/wave-networks-core",
      "deploys_to": "public_html/admin/",
      "purpose": "Auth, users, sessions, shard routing, email queue, notifications, shared helpers"
    },
    "child_app_template": {
      "url": "https://github.com/WaveNetworks/child-app",
      "deploys_to": "public_html/child-app/",
      "purpose": "Template for building apps. Clone, rename, build features on top."
    },
    "website_template": {
      "url": "https://github.com/WaveNetworks/subtheme-website",
      "deploys_to": "public_html/ (root + site/)",
      "purpose": "Reference public-facing website. Clone this to create a marketing/documentation site for any child app."
    }
  },

  "requirements": {
    "php": ">=8.2",
    "mysql": ">=5.7",
    "composer": true,
    "extensions": ["pdo_mysql", "gd", "mbstring"],
    "optional": ["imagick"],
    "build_tools": ["nodejs (for SCSS compilation only)"]
  },

  "setup": {
    "docker": [
      "CRITICAL: Clone the child-app template repo as your-app-name/ — never create a child app directory from scratch. All child apps are forks of this template: git clone https://github.com/WaveNetworks/child-app.git your-app-name/",
      "Clone admin repo as admin/ (if not already present)",
      "cd your-app-name && cp .env.example .env",
      "Edit .env with database names and secrets",
      "docker compose up -d",
      "Visit http://localhost/admin/install/index.php to set up admin core (Step 1: DB config with 1-20 shards, Step 2: create first admin user)",
      "Visit http://localhost/your-app-name/install/index.php to set up child app",
      "Migrations run automatically on first page load"
    ],
    "shared_hosting": [
      "CRITICAL: Clone the child-app template repo as your-app-name/ — never create a child app directory from scratch: git clone https://github.com/WaveNetworks/child-app.git your-app-name/",
      "Run composer install --no-dev in admin/ locally",
      "Upload admin/ and your-app-name/ to public_html/ via SFTP",
      "Create 6 databases: wncore_main, wncore_shard_1, wncore_shard_2 (admin), yourapp_main, yourapp_shard_1, yourapp_shard_2 (child app)",
      "Create ../files/ directory above public_html (chmod 755)",
      "Visit https://yourdomain.com/admin/install/index.php — configure DB (1-20 shards), files path (auto-detected), SMTP, then create first admin user",
      "Visit https://yourdomain.com/your-app-name/install/index.php — enter child DB credentials (1-20 shards)",
      "Set up cron: * * * * * /usr/bin/php public_html/admin/cron/cron.php"
    ]
  },

  "architecture": {
    "bootstrap_chain": "child-app/include/common.php includes ../../admin/include/common.php",
    "bootstrap_order": [
      "Composer autoload",
      "Load config (config.php or env vars)",
      "Open $db PDO connection to main DB",
      "Glob-include all helpers from include/common/",
      "Run admin migrations (check_and_migrate_main_db)",
      "Run shard migrations (check_and_migrate_all_shards)",
      "Start session + guard (redirect to login if unauthenticated)",
      "Include definition.php",
      "Glob-include all action files",
      "Then child app: load own config, open $child_db, run own migrations, include own helpers/actions"
    ],
    "databases": {
      "core_main": {
        "variable": "$db",
        "access": "read-only from child apps",
        "tables": ["user", "device", "api_key", "forgot", "invite", "auth_settings", "email_queue", "email_settings", "email_allowed_sender", "notification_category", "oauth_provider", "saml_provider", "registered_theme", "error_log", "service_api_key", "consent_version", "user_consent", "login_history", "account_deletion_request", "data_export_request", "cron_log", "db_version"]
      },
      "child_main": {
        "variable": "$child_db",
        "access": "read/write",
        "description": "Child app's own tables (items, settings, whatever you build)"
      },
      "core_shards": {
        "variable": "$shardConfigs",
        "access": "read-only from child apps",
        "tables": ["user_profile", "notification", "notification_preference", "push_subscription", "db_version"]
      },
      "child_shards": {
        "variable": "$childShardConfigs",
        "access": "read/write",
        "description": "Per-user data distributed across shard databases"
      }
    },
    "shard_routing": {
      "login_flow": "Authenticate on main DB → store user_id + shard_id in session → prime_shard() opens shard connection",
      "new_user_assignment": "SELECT shard_id, COUNT(*) FROM user GROUP BY shard_id ORDER BY cnt ASC LIMIT 1",
      "shard_id_immutable": true
    }
  },

  "coding_rules": {
    "do": [
      "Escape out of PHP for HTML — never echo HTML strings",
      "Use <?= $var ?> shorthand, always h() for user-supplied data",
      "Use sanitize($val, SQL) for DB values or prepared statements",
      "Flat if ($_POST['action'] == 'x') blocks in action files",
      "Collect ALL validation errors in $errs array before setting $_SESSION['error']",
      "Always set $_SESSION['success'] when an action completes",
      "New helpers go in include/common/ — glob picks them up automatically",
      "Start every new child app by cloning the child-app template repo — never create the directory structure manually",
      "Use __DIR__-based paths for all includes",
      "Use IF NOT EXISTS in CREATE TABLE (makes migrations rerunnable)",
      "Wrap migrations in START TRANSACTION / COMMIT"
    ],
    "do_not": [
      "Write to core DB tables from child app code",
      "Create new API endpoint files — use action files",
      "Echo HTML strings from PHP",
      "Skip h() or sanitize() for any output",
      "Edit vendor/ directory",
      "Edit existing migration files — create a new version",
      "Set $_SESSION['error'] on first validation failure (collect all first)",
      "Create a new $db PDO connection — use the global $db",
      "Store credentials outside config/config.php (gitignored)",
      "Use absolute asset paths",
      "Define $db_version before including admin's common.php",
      "Name your DB variable $db — use $child_db"
    ]
  },

  "patterns": {
    "action_file": "if ($_POST['action'] == 'addItem') {\n    $errs = [];\n    if (!$_SESSION['user_id']) { $errs['auth'] = 'Login required.'; }\n    if (!$_POST['title'])      { $errs['title'] = 'Title required.'; }\n    if (count($errs) <= 0) {\n        child_db_query_prepared(\"INSERT INTO items ...\", [...]);\n        $_SESSION['success'] = 'Done.';\n    } else {\n        $_SESSION['error'] = implode('<br>', $errs);\n    }\n}",
    "view_template": "<?php if ($someCondition) { ?>\n<div class=\"something\"><?= h($userData['field']) ?></div>\n<?php } ?>",
    "api_response": "{ \"error\": \"\", \"success\": \"Action completed.\", \"info\": \"\", \"warning\": \"\", \"results\": {} }",
    "migration_sql": "-- Migration X.X for [Main/Shard] Database\nSET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\nSTART TRANSACTION;\nSET time_zone = \"+00:00\";\n-- SQL here\nCOMMIT;",
    "form_post": "<form method=\"post\">\n    <input type=\"hidden\" name=\"action\" value=\"addItem\">\n    <!-- fields -->\n    <button type=\"submit\">Save</button>\n</form>",
    "ajax_call": "apiPost('actionName', { key: 'value' }, function(response) {\n    // response.results contains $data from action file\n});"
  },

  "functions": {
    "child_db_query": {
      "signature": "child_db_query($sql)",
      "returns": "PDOStatement",
      "description": "Query child app's main database"
    },
    "child_db_query_prepared": {
      "signature": "child_db_query_prepared($sql, $params)",
      "returns": "PDOStatement",
      "description": "Prepared query on child app's main database"
    },
    "child_db_insert_id": {
      "signature": "child_db_insert_id()",
      "returns": "string",
      "description": "Last insert ID on child main DB"
    },
    "child_prime_shard": {
      "signature": "child_prime_shard($shard_id)",
      "returns": "PDO|false",
      "description": "Open/cache child shard database connection"
    },
    "child_db_query_shard": {
      "signature": "child_db_query_shard($shard_id, $sql)",
      "returns": "PDOStatement",
      "description": "Query child app's shard database"
    },
    "child_db_query_shard_prepared": {
      "signature": "child_db_query_shard_prepared($shard_id, $sql, $params)",
      "returns": "PDOStatement",
      "description": "Prepared query on child app's shard database"
    },
    "queue_email": {
      "signature": "queue_email($to_email, $to_name, $subject, $body_html, $opts = [])",
      "returns": "int|false",
      "description": "Queue email via core's SMTP service. Returns queue_id on success, false on failure. Options: from_email, from_name, reply_to, source_app"
    },
    "send_notification": {
      "signature": "send_notification($user_id, $shard_id, $category_slug, $title, $body, $options = [])",
      "returns": "int|false",
      "description": "Send notification to a specific user. Options: action_url, action_label, source_app"
    },
    "broadcast_notification": {
      "signature": "broadcast_notification($category_slug, $title, $body, $options = [])",
      "returns": "int",
      "description": "Send notification to all users across all shards"
    },
    "register_notification_category": {
      "signature": "register_notification_category($slug, $name, $description = '', $opts = [])",
      "returns": "void",
      "description": "Register a notification category (idempotent). Options: icon, default_frequency, created_by_app"
    },
    "h": {
      "signature": "h($string)",
      "returns": "string",
      "description": "HTML-escape output (htmlspecialchars with ENT_QUOTES, UTF-8)"
    },
    "sanitize": {
      "signature": "sanitize($value, $type)",
      "returns": "string",
      "description": "Sanitize input. Use sanitize($val, SQL) for database values"
    },
    "db_query": {
      "signature": "db_query($sql)",
      "returns": "PDOStatement",
      "description": "Query core main database (read-only from child apps)"
    },
    "db_fetch": {
      "signature": "db_fetch($statement)",
      "returns": "array|false",
      "description": "Fetch associative row from PDOStatement"
    },
    "prime_shard": {
      "signature": "prime_shard($shard_id)",
      "returns": "PDO|false",
      "description": "Open/cache core shard database connection"
    },
    "db_query_shard": {
      "signature": "db_query_shard($shard_id, $sql)",
      "returns": "PDOStatement",
      "description": "Query core shard database (read-only recommended from child apps)"
    },
    "get_branding": {
      "signature": "get_branding()",
      "returns": "array",
      "description": "Get branding settings (site_name, logo_path, favicon_path, theme_color, etc.)"
    },
    "apiPost": {
      "signature": "apiPost(action, data, callback)",
      "type": "javascript",
      "description": "AJAX POST to api/index.php. Returns { error, success, info, warning, results }"
    },
    "modalView": {
      "signature": "modalView(params)",
      "type": "javascript",
      "description": "Load AJAX content into #largeModal. Params: 'modal=snippet_name&param=value'"
    },
    "record_consent": {
      "signature": "record_consent($user_id, $consent_type, $action, $version_id = null)",
      "returns": "void",
      "description": "Append consent event to immutable audit log (granted or withdrawn)"
    },
    "get_consent_status": {
      "signature": "get_consent_status($user_id, $consent_type)",
      "returns": "string|null",
      "description": "Get latest consent status: 'granted', 'withdrawn', or null"
    },
    "get_all_consent_statuses": {
      "signature": "get_all_consent_statuses($user_id)",
      "returns": "array",
      "description": "All consent types with their current status for a user"
    },
    "get_consent_history": {
      "signature": "get_consent_history($user_id)",
      "returns": "array",
      "description": "Full consent audit trail with version labels"
    },
    "request_data_export": {
      "signature": "request_data_export($user_id, $format = 'json')",
      "returns": "int|false",
      "description": "Request GDPR data export in JSON or CSV format"
    },
    "build_export_data": {
      "signature": "build_export_data($user_id, $shard_id)",
      "returns": "array",
      "description": "Collect user data from admin main + shard DBs for export"
    },
    "request_account_deletion": {
      "signature": "request_account_deletion($user_id, $reason = '')",
      "returns": "int|false",
      "description": "Request GDPR account deletion with 30-day cooling-off period"
    },
    "cancel_account_deletion": {
      "signature": "cancel_account_deletion($user_id)",
      "returns": "void",
      "description": "Cancel pending account deletion request"
    },
    "record_login": {
      "signature": "record_login($user_id, $method = 'password', $status = 'success')",
      "returns": "void",
      "description": "Record login attempt in login_history (called automatically by login flow)"
    },
    "get_login_history": {
      "signature": "get_login_history($user_id, $limit = 20, $offset = 0)",
      "returns": "array",
      "description": "Paginated login history for a user"
    },
    "get_user_devices": {
      "signature": "get_user_devices($user_id)",
      "returns": "array",
      "description": "List all registered devices/sessions for a user"
    },
    "revoke_device": {
      "signature": "revoke_device($device_id, $user_id)",
      "returns": "void",
      "description": "Revoke a single device session (deletes device + API key)"
    },
    "revoke_all_other_devices": {
      "signature": "revoke_all_other_devices($user_id, $current_cookie_id = null)",
      "returns": "void",
      "description": "Revoke all device sessions except the current one"
    },
    "check_reconsent_needed": {
      "signature": "check_reconsent_needed($user_id)",
      "returns": "array",
      "description": "Check if user needs to re-accept updated Terms of Service or Privacy Policy"
    },
    "db_query_prepared": {
      "signature": "db_query_prepared($sql, $params)",
      "returns": "PDOStatement",
      "description": "Prepared query on core main database"
    },
    "db_insert_id": {
      "signature": "db_insert_id()",
      "returns": "string",
      "description": "Last insert ID on core main database"
    },
    "db_fetch_all": {
      "signature": "db_fetch_all($statement)",
      "returns": "array",
      "description": "Fetch all rows from PDOStatement as associative arrays"
    },
    "db_num_rows": {
      "signature": "db_num_rows($statement)",
      "returns": "int",
      "description": "Row count from PDOStatement"
    },
    "db_query_shard_prepared": {
      "signature": "db_query_shard_prepared($shard_id, $sql, $params)",
      "returns": "PDOStatement",
      "description": "Prepared query on core shard database"
    },
    "child_shard_insert_id": {
      "signature": "child_shard_insert_id($shard_id)",
      "returns": "string",
      "description": "Last insert ID on child shard database"
    },
    "register_theme": {
      "signature": "register_theme($slug, $name, $css_path, $opts = [])",
      "returns": "void",
      "description": "Register a custom theme in admin's theme registry (idempotent). Options: sidebar_mode, created_by_app, is_active"
    },
    "slugify": {
      "signature": "slugify($string)",
      "returns": "string",
      "description": "Convert string to URL-safe slug (lowercase, hyphens)"
    },
    "redirect": {
      "signature": "redirect($url)",
      "returns": "void",
      "description": "Send Location header and exit"
    },
    "get_pending_deletion": {
      "signature": "get_pending_deletion($user_id)",
      "returns": "array|null",
      "description": "Get active pending account deletion request or null"
    },
    "get_latest_export": {
      "signature": "get_latest_export($user_id)",
      "returns": "array|false",
      "description": "Get latest data export request for a user"
    },
    "complete_data_export": {
      "signature": "complete_data_export($export_id, $file_path, $file_size)",
      "returns": "void",
      "description": "Mark data export as ready with 7-day expiry"
    },
    "count_login_history": {
      "signature": "count_login_history($user_id)",
      "returns": "int",
      "description": "Total login history count for a user"
    }
  },

  "shared_services": {
    "email": {
      "function": "queue_email($to_email, $to_name, $subject, $body_html, $opts = [])",
      "config_location": "admin/config/config.php or SMTP_* env vars",
      "features": ["SMTP queue", "per-minute/hour throttling", "allowed sender management", "delivery logging"]
    },
    "notifications": {
      "functions": ["register_notification_category()", "send_notification()", "broadcast_notification()"],
      "storage": "Per-user on shard database",
      "preferences": ["realtime", "daily", "weekly", "off"],
      "push": "VAPID Web Push via minishlink/web-push"
    },
    "cron": {
      "main_runner": "admin/cron/cron.php — every minute",
      "tasks": ["Email queue processing", "Push digest (daily/weekly)", "Notification cleanup (monthly)", "Custom tasks in cron/minutes/, days/, weeks/, months/"]
    },
    "compliance": {
      "functions": ["record_consent()", "get_consent_status()", "get_consent_history()", "request_data_export()", "build_export_data()", "request_account_deletion()", "cancel_account_deletion()", "record_login()", "get_login_history()", "get_user_devices()", "revoke_device()", "revoke_all_other_devices()", "check_reconsent_needed()"],
      "storage": "All compliance data stored in admin main DB (centralized across all child apps)",
      "features": ["Consent versioning with re-consent enforcement", "Immutable consent audit trail", "Data export (JSON/CSV) with 7-day expiry", "Account deletion with 30-day cooling-off", "Login history tracking", "Device session management with remote revocation", "Admin compliance dashboard"]
    }
  },

  "file_structure": {
    "admin/include/common.php": "Full authenticated bootstrap",
    "admin/include/common_auth.php": "Auth page bootstrap (no session guard)",
    "admin/include/common_api.php": "API bootstrap",
    "admin/include/common_readonly.php": "Cron bootstrap (no actions, no session)",
    "admin/config/config.php": "Gitignored credentials (shared hosting)",
    "admin/config/config.sample.php": "Template for config.php",
    "admin/db_migrations/main/": "Main DB migration SQL files",
    "admin/db_migrations/shard/": "Shard DB migration SQL files",
    "admin/include/actions/memberActions/": "Authenticated action files",
    "admin/include/actions/apiActions/": "AJAX/API action files",
    "admin/include/actions/loginActions/": "Auth flow action files",
    "admin/include/common/": "Helper functions (glob-included)",
    "admin/views/": "Admin UI pages",
    "admin/auth/": "Login, register, OAuth, SAML, 2FA pages",
    "admin/assets/css/style.css": "Sidebar, navbar, layout CSS",
    "admin/assets/js/bs-init.js": "apiPost() AJAX helper",
    "admin/cron/cron.php": "Main cron runner",
    "child-app/include/common.php": "Child app bootstrap (includes admin's common.php first)",
    "child-app/include/common/childDBFunctions.php": "$child_db and shard wrappers",
    "child-app/include/common/appMigrationFunctions.php": "Child migration runner",
    "child-app/views/template.php": "HTML shell (sidebar, topnav, footer)",
    "child-app/db_migrations/main/": "Child app main DB migrations",
    "child-app/db_migrations/shard/": "Child app shard DB migrations",
    "child-app/assets/scss/custom.scss": "SCSS theme entry point"
  },

  "docker_multi_app": {
    "description": "Each child app runs as a fully independent Docker Compose stack with its own admin databases, its own admin install, and its own branding. Source code is mounted read-only (:ro) so Docker cannot modify your working directory.",
    "why_independent": "Every child app includes its own admin stack. All databases are namespaced by the child app name so multiple stacks never collide.",
    "filesystem_isolation": {
      "source_mounts": "Both admin/ and child-app/ are mounted :ro. Edit locally, refresh browser.",
      "writable_volumes": "Named volumes for admin_vendor (composer deps), files_vol (uploads), and all database volumes."
    },
    "naming_convention": {
      "rule": "All database names and volumes are prefixed with the child app directory name (hyphens → underscores). Template uses child_app_ — rename to yourapp_ when cloning.",
      "database_names": {
        "child_app_admin_main": "Admin main DB",
        "child_app_admin_shard_1": "Admin shard 1",
        "child_app_admin_shard_2": "Admin shard 2",
        "child_app_main": "Child app main DB",
        "child_app_shard_1": "Child app shard 1",
        "child_app_shard_2": "Child app shard 2"
      },
      "example_rename": "Clone as payments/ → rename child_app_ to payments_ everywhere in docker-compose.yml",
      "warning": "Never mark volumes as external: true. Each app stack must own its volumes."
    },
    "port_allocation": {
      "rule": "Each stack needs unique host ports.",
      "example": "App 1: 80 + mailhog 8025. App 2: 8081:80 + mailhog 8026:8025. App 3: 8082:80 + mailhog 8027:8025."
    },
    "install_notes": {
      "url": "Use /admin/install/index.php explicitly — /admin/install/ may return 403 depending on Apache DirectoryIndex config.",
      "two_steps": "The admin installer is a 2-step process: Step 1 configures databases (1-20 shards), file storage (auto-detected path), and SMTP. Step 2 creates the first admin user with email and password — no need to register separately or run SQL.",
      "readonly_safe": "Source is mounted :ro, so the installer cannot write install/.htaccess. In Docker (ENVIRONMENT=development), installer lockdown is skipped automatically.",
      "php_preflight": "The installer checks PHP version (8.2+ required) and required extensions (pdo_mysql, gd, mbstring) before showing the form.",
      "files_auto_create": "The installer auto-detects the files directory path and creates it (including files/home/) if it doesn't exist."
    },
    "scss_build": "The template ships SCSS source only — no compiled CSS. After cloning, run: npm install && npm run build:css to compile themes in assets/themes/."
  },

  "public_website": {
    "description": "Every child app can have a public-facing marketing and documentation website. The subtheme-website repo is the reference implementation — clone it to create a website for your app.",
    "template_repo": "https://github.com/WaveNetworks/subtheme-website",
    "reference_site": "https://subtheme.com",
    "pattern": {
      "standalone": "Plain PHP + HTML + CSS. No framework, no build tools, no npm, no database writes.",
      "bootstrap": "Uses admin's common_auth.php (NOT common.php) for branding. common_auth.php provides get_branding(), h(), and DB access WITHOUT requiring a login session.",
      "css_stack": ["admin/assets/css/style.css", "admin/assets/css/bs-theme-overrides.css", "child-app/assets/css/custom.css (glassmorphism)", "bootstrap-icons CDN", "site/style.css (your overrides)"],
      "js_stack": ["Bootstrap 5 bundle (CDN)", "admin/assets/js/color-mode.js", "site/js/theme-site.js (theme switcher)", "child-app/assets/js/bg-canvas.js (animated background)"],
      "deployment": "Separate git repo deployed to public_html/ root. Not part of admin or child-app repos.",
      "docs_system": "Section-based routing via .htaccess: /docs/{section} → site/docs.php?section={section}. Each section is a PHP partial in site/docs/."
    },
    "key_rules": [
      "Include common_auth.php, NEVER common.php — common.php redirects unauthenticated users to login",
      "Use get_branding() for dynamic logo, site name, favicon, theme color",
      "Use h() for all output (available via common_auth.php)",
      "Use absolute paths for cross-repo assets (/admin/assets/, /child-app/assets/)",
      "No authentication required on any page",
      "No database writes — read-only access to admin's branding data",
      "Keep all CSS in one file (site/style.css), no build tools",
      "Support both dark and light mode via [data-bs-theme] overrides"
    ],
    "clone_workflow": [
      "git clone https://github.com/WaveNetworks/subtheme-website.git your-site/",
      "Update site/bootstrap.php to point to your admin's common_auth.php path",
      "Update branding fallbacks in bootstrap.php with your app name",
      "Replace marketing content in index.php with your app's content",
      "Replace docs sections in site/docs/ with your app's documentation",
      "Update .htaccess URL routing for your domain",
      "Deploy to public_html/ on your production server"
    ]
  },

  "deployment": {
    "deploy_script": {
      "location": "deploy.py in each repo root (admin/, child-app/)",
      "requires": "pip install paramiko",
      "config_file": "deploy_config.py (copy from deploy_config.sample.py)",
      "config_format": {
        "SFTP_HOST": "your-server.example.com",
        "SFTP_PORT": 22,
        "SFTP_USER": "username",
        "SFTP_PASS": "password",
        "LOCAL_ROOT": "/path/to/local/repo",
        "REMOTE_ROOT": "admin or child-app (relative to SFTP root)"
      },
      "flags": {
        "--all": "Deploy all tracked files + vendor directory",
        "--dry-run": "Show what would be uploaded without uploading",
        "--vendor": "Deploy only vendor/ directory",
        "--include PATH [PATH ...]": "Include specific files or directories (even untracked)",
        "--no-css-check": "(child-app only) Skip the CSS build warning"
      },
      "behavior": [
        "Default: uploads files changed in last git commit (git diff HEAD~1 HEAD)",
        "Compares file sizes before uploading — skips unchanged files",
        "Auto-creates REMOTE_ROOT and subdirectories on the server",
        "Respects EXCLUDE_PATTERNS list (no .git, .env, node_modules, tests, etc.)",
        "Child-app deploy warns if assets/css/custom.css is missing (needs npm run build:css)"
      ]
    },
    "deploy_order": [
      "1. admin/ (wave-networks-core) — must be deployed and installed first",
      "2. child-app/ — depends on admin being installed for DB and auth",
      "3. website (public_html root + site/) — depends on admin branding and child-app CSS"
    ],
    "production_checklist": [
      "Verify PHP 8.2+ on server",
      "composer install --no-dev in admin/",
      "npm install && npm run build:css in child-app/ (builds glassmorphism theme)",
      "Create deploy_config.py in each repo from sample",
      "python deploy.py --dry-run (verify file list)",
      "python deploy.py --all (first deploy) or python deploy.py (subsequent)",
      "python deploy.py --vendor (admin only, after composer install)",
      "Run admin installer: /admin/install/index.php",
      "Run child-app installer: /your-app/install/index.php",
      "Set up cron job for email queue",
      "Configure branding via admin settings",
      "Verify in browser"
    ],
    "branding": {
      "form_location": "admin/views/settings.php",
      "field_names": {
        "site_name": "Site display name",
        "site_short_name": "Short name (PWA manifest, browser tab)",
        "site_description": "Site description",
        "theme_color": "Theme color hex value (PWA, browser chrome)",
        "logo": "Logo file upload (displayed in navbar, login page)",
        "logo_dark": "Dark mode logo variant",
        "favicon": "Favicon file upload"
      },
      "storage": "auth_settings table in admin main DB",
      "access": "get_branding() returns all values — available to child apps and websites via common_auth.php"
    },
    "website_deployment": {
      "note": "The website (site/ directory) is NOT in admin or child-app repos",
      "method": "Upload via SFTP using paramiko (no deploy.py — not git-tracked)",
      "remote_paths": "Mirror local paths relative to public_html/"
    }
  },

  "migration_rules": {
    "two_step_process": "1. Create SQL file in db_migrations/main/ or shard/. 2. Update $child_db_version or $child_shard_version in include/common.php",
    "version_format": "Decimal: 1.0, 1.1, 1.2, 2.0",
    "child_versions_start_at": "1.0",
    "independent_from_admin": true,
    "auto_runs_on_page_load": true,
    "never_edit_existing_files": true,
    "use_if_not_exists": true,
    "wrap_in_transaction": true
  }
}
