5683 lines
108 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Build Power. Not Rent It. Own your digital infrastructure.">
<meta name="author" content="Bunker Operations">
<link rel="canonical" href="https://bnkserve.org/v2/architecture/">
<link rel="prev" href="../getting-started/quick-start/">
<link rel="next" href="dual-api/">
<link rel="icon" href="../../assets/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.1">
<title>V2 Architecture Overview - Changemaker Lite</title>
<link rel="stylesheet" href="../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<link rel="stylesheet" href="../../stylesheets/extra.css">
<link rel="stylesheet" href="../../stylesheets/home.css">
<link rel="stylesheet" href="../../assets/css/video-player.css">
<script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
<meta property="og:type" content="website" />
<meta property="og:title" content="V2 Architecture Overview - Changemaker Lite" />
<meta property="og:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="og:image" content="https://bnkserve.org/assets/images/social/v2/architecture/index.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://bnkserve.org/v2/architecture/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="V2 Architecture Overview - Changemaker Lite" />
<meta property="twitter:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="twitter:image" content="https://bnkserve.org/assets/images/social/v2/architecture/index.png" />
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#v2-architecture-overview" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../assets/logo.png" alt="logo">
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Changemaker Lite
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
V2 Architecture Overview
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31"/></svg>
</label>
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z"/></svg>
</label>
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
</nav>
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../" class="md-tabs__link">
V2 Documentation
</a>
</li>
<li class="md-tabs__item">
<a href="../../phil/" class="md-tabs__link">
Philosophy
</a>
</li>
<li class="md-tabs__item">
<a href="../../v1/" class="md-tabs__link">
V1 Documentation (Legacy)
</a>
</li>
<li class="md-tabs__item">
<a href="../../blog/" class="md-tabs__link">
Blog
</a>
</li>
</ul>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../.." title="Changemaker Lite" class="md-nav__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../assets/logo.png" alt="logo">
</a>
Changemaker Lite
</label>
<div class="md-nav__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../" class="md-nav__link ">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
<label class="md-nav__link " for="__nav_2" id="__nav_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2">
<span class="md-nav__icon md-icon"></span>
V2 Documentation
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_2" >
<div class="md-nav__link md-nav__container">
<a href="../getting-started/" class="md-nav__link ">
<span class="md-ellipsis">
Getting Started
</span>
</a>
<label class="md-nav__link " for="__nav_2_2" id="__nav_2_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_2_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_2">
<span class="md-nav__icon md-icon"></span>
Getting Started
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../getting-started/quick-start/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_3" checked>
<div class="md-nav__link md-nav__container">
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Architecture
</span>
</a>
<label class="md-nav__link md-nav__link--active" for="__nav_2_3" id="__nav_2_3_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_3_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_3">
<span class="md-nav__icon md-icon"></span>
Architecture
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="dual-api/" class="md-nav__link">
<span class="md-ellipsis">
Dual API System
</span>
</a>
</li>
<li class="md-nav__item">
<a href="authentication/" class="md-nav__link">
<span class="md-ellipsis">
Authentication & Security
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_4" >
<div class="md-nav__link md-nav__container">
<a href="../backend/" class="md-nav__link ">
<span class="md-ellipsis">
Backend
</span>
</a>
<label class="md-nav__link " for="__nav_2_4" id="__nav_2_4_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_4_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_4">
<span class="md-nav__icon md-icon"></span>
Backend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../backend/modules/" class="md-nav__link">
<span class="md-ellipsis">
Modules
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../backend/services/" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../backend/middleware/" class="md-nav__link">
<span class="md-ellipsis">
Middleware
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../backend/utilities/" class="md-nav__link">
<span class="md-ellipsis">
Utilities
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_5" >
<div class="md-nav__link md-nav__container">
<a href="../frontend/" class="md-nav__link ">
<span class="md-ellipsis">
Frontend
</span>
</a>
<label class="md-nav__link " for="__nav_2_5" id="__nav_2_5_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_5">
<span class="md-nav__icon md-icon"></span>
Frontend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../frontend/components/" class="md-nav__link">
<span class="md-ellipsis">
Components
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../frontend/layouts/" class="md-nav__link">
<span class="md-ellipsis">
Layouts
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../frontend/pages/" class="md-nav__link">
<span class="md-ellipsis">
Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_6" >
<div class="md-nav__link md-nav__container">
<a href="../database/" class="md-nav__link ">
<span class="md-ellipsis">
Database
</span>
</a>
<label class="md-nav__link " for="__nav_2_6" id="__nav_2_6_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_6">
<span class="md-nav__icon md-icon"></span>
Database
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../database/schema/" class="md-nav__link">
<span class="md-ellipsis">
Schema Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../database/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../database/seeding/" class="md-nav__link">
<span class="md-ellipsis">
Seeding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../database/indexes/" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../database/models/" class="md-nav__link">
<span class="md-ellipsis">
Models
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_7" >
<div class="md-nav__link md-nav__container">
<a href="../features/" class="md-nav__link ">
<span class="md-ellipsis">
Features
</span>
</a>
<label class="md-nav__link " for="__nav_2_7" id="__nav_2_7_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_7_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_7">
<span class="md-nav__icon md-icon"></span>
Features
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../features/influence/" class="md-nav__link">
<span class="md-ellipsis">
Influence
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../features/map/" class="md-nav__link">
<span class="md-ellipsis">
Map
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../features/landing-pages/" class="md-nav__link">
<span class="md-ellipsis">
Landing Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../features/email-templates/" class="md-nav__link">
<span class="md-ellipsis">
Email Templates
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../features/media/" class="md-nav__link">
<span class="md-ellipsis">
Media
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../features/newsletter/" class="md-nav__link">
<span class="md-ellipsis">
Newsletter
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../features/observability/" class="md-nav__link">
<span class="md-ellipsis">
Observability
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../features/tunnel/" class="md-nav__link">
<span class="md-ellipsis">
Tunnel
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_8" >
<div class="md-nav__link md-nav__container">
<a href="../deployment/" class="md-nav__link ">
<span class="md-ellipsis">
Deployment
</span>
</a>
<label class="md-nav__link " for="__nav_2_8" id="__nav_2_8_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_8_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_8">
<span class="md-nav__icon md-icon"></span>
Deployment
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../deployment/docker-compose/" class="md-nav__link">
<span class="md-ellipsis">
Docker Compose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../deployment/environment-variables/" class="md-nav__link">
<span class="md-ellipsis">
Environment Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../deployment/nginx/" class="md-nav__link">
<span class="md-ellipsis">
Nginx Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../deployment/ssl-tls/" class="md-nav__link">
<span class="md-ellipsis">
SSL/TLS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../deployment/tunneling/" class="md-nav__link">
<span class="md-ellipsis">
Tunneling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../deployment/monitoring-stack/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Stack
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../deployment/healthchecks/" class="md-nav__link">
<span class="md-ellipsis">
Health Checks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../deployment/scaling/" class="md-nav__link">
<span class="md-ellipsis">
Scaling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../deployment/backup-restore/" class="md-nav__link">
<span class="md-ellipsis">
Backup & Restore
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_9" >
<div class="md-nav__link md-nav__container">
<a href="../development/" class="md-nav__link ">
<span class="md-ellipsis">
Development
</span>
</a>
<label class="md-nav__link " for="__nav_2_9" id="__nav_2_9_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_9_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_9">
<span class="md-nav__icon md-icon"></span>
Development
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../development/local-setup/" class="md-nav__link">
<span class="md-ellipsis">
Local Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../development/docker-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Docker Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../development/git-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Git Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../development/npm-commands/" class="md-nav__link">
<span class="md-ellipsis">
NPM Commands
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../development/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../development/typescript/" class="md-nav__link">
<span class="md-ellipsis">
TypeScript
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../development/testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../development/debugging/" class="md-nav__link">
<span class="md-ellipsis">
Debugging
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../development/code-style/" class="md-nav__link">
<span class="md-ellipsis">
Code Style
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_10" >
<div class="md-nav__link md-nav__container">
<a href="../api-reference/" class="md-nav__link ">
<span class="md-ellipsis">
API Reference
</span>
</a>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_10_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_10">
<span class="md-nav__icon md-icon"></span>
API Reference
</label>
<ul class="md-nav__list" data-md-scrollfix>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_11" >
<div class="md-nav__link md-nav__container">
<a href="../user-guides/" class="md-nav__link ">
<span class="md-ellipsis">
User Guides
</span>
</a>
<label class="md-nav__link " for="__nav_2_11" id="__nav_2_11_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_11_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_11">
<span class="md-nav__icon md-icon"></span>
User Guides
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../user-guides/admin-guide/" class="md-nav__link">
<span class="md-ellipsis">
Admin Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../user-guides/campaign-manager-guide/" class="md-nav__link">
<span class="md-ellipsis">
Campaign Manager Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../user-guides/map-organizer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Map Organizer Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../user-guides/content-editor-guide/" class="md-nav__link">
<span class="md-ellipsis">
Content Editor Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../user-guides/volunteer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Guide
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_12" >
<div class="md-nav__link md-nav__container">
<a href="../troubleshooting/" class="md-nav__link ">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<label class="md-nav__link " for="__nav_2_12" id="__nav_2_12_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_12_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_12">
<span class="md-nav__icon md-icon"></span>
Troubleshooting
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../troubleshooting/faq/" class="md-nav__link">
<span class="md-ellipsis">
FAQ
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../troubleshooting/common-errors/" class="md-nav__link">
<span class="md-ellipsis">
Common Errors
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../troubleshooting/auth-issues/" class="md-nav__link">
<span class="md-ellipsis">
Auth Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../troubleshooting/database-issues/" class="md-nav__link">
<span class="md-ellipsis">
Database Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../troubleshooting/docker-issues/" class="md-nav__link">
<span class="md-ellipsis">
Docker Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../troubleshooting/email-issues/" class="md-nav__link">
<span class="md-ellipsis">
Email Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../troubleshooting/geocoding-issues/" class="md-nav__link">
<span class="md-ellipsis">
Geocoding Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../troubleshooting/monitoring-issues/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../troubleshooting/performance-optimization/" class="md-nav__link">
<span class="md-ellipsis">
Performance Optimization
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_13" >
<div class="md-nav__link md-nav__container">
<a href="../migration/" class="md-nav__link ">
<span class="md-ellipsis">
Migration
</span>
</a>
<label class="md-nav__link " for="__nav_2_13" id="__nav_2_13_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_13_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_13">
<span class="md-nav__icon md-icon"></span>
Migration
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../migration/feature-parity/" class="md-nav__link">
<span class="md-ellipsis">
Feature Parity
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../migration/breaking-changes/" class="md-nav__link">
<span class="md-ellipsis">
Breaking Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../migration/api-changes/" class="md-nav__link">
<span class="md-ellipsis">
API Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../migration/data-migration/" class="md-nav__link">
<span class="md-ellipsis">
Data Migration
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_14" >
<div class="md-nav__link md-nav__container">
<a href="../contributing/" class="md-nav__link ">
<span class="md-ellipsis">
Contributing
</span>
</a>
<label class="md-nav__link " for="__nav_2_14" id="__nav_2_14_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_14_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_14">
<span class="md-nav__icon md-icon"></span>
Contributing
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../contributing/development-setup/" class="md-nav__link">
<span class="md-ellipsis">
Development Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../contributing/code-of-conduct/" class="md-nav__link">
<span class="md-ellipsis">
Code of Conduct
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../contributing/pull-requests/" class="md-nav__link">
<span class="md-ellipsis">
Pull Requests
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../contributing/roadmap/" class="md-nav__link">
<span class="md-ellipsis">
Roadmap
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../phil/" class="md-nav__link">
<span class="md-ellipsis">
Philosophy
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../v1/" class="md-nav__link">
<span class="md-ellipsis">
V1 Documentation (Legacy)
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../blog/" class="md-nav__link">
<span class="md-ellipsis">
Blog
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#system-architecture" class="md-nav__link">
<span class="md-ellipsis">
System Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#core-components" class="md-nav__link">
<span class="md-ellipsis">
Core Components
</span>
</a>
<nav class="md-nav" aria-label="Core Components">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#1-nginx-reverse-proxy" class="md-nav__link">
<span class="md-ellipsis">
1. Nginx Reverse Proxy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-frontend-layer" class="md-nav__link">
<span class="md-ellipsis">
2. Frontend Layer
</span>
</a>
<nav class="md-nav" aria-label="2. Frontend Layer">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#admin-gui-port-3000" class="md-nav__link">
<span class="md-ellipsis">
Admin GUI (Port 3000)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#public-pages" class="md-nav__link">
<span class="md-ellipsis">
Public Pages
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#volunteer-portal" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Portal
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#3-backend-layer-dual-api-design" class="md-nav__link">
<span class="md-ellipsis">
3. Backend Layer - Dual API Design
</span>
</a>
<nav class="md-nav" aria-label="3. Backend Layer - Dual API Design">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#express-api-port-4000" class="md-nav__link">
<span class="md-ellipsis">
Express API (Port 4000)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fastify-api-port-4100" class="md-nav__link">
<span class="md-ellipsis">
Fastify API (Port 4100)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#4-data-layer" class="md-nav__link">
<span class="md-ellipsis">
4. Data Layer
</span>
</a>
<nav class="md-nav" aria-label="4. Data Layer">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#postgresql-16" class="md-nav__link">
<span class="md-ellipsis">
PostgreSQL 16
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#redis" class="md-nav__link">
<span class="md-ellipsis">
Redis
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#5-job-processing" class="md-nav__link">
<span class="md-ellipsis">
5. Job Processing
</span>
</a>
<nav class="md-nav" aria-label="5. Job Processing">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#bullmq-queues" class="md-nav__link">
<span class="md-ellipsis">
BullMQ Queues
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#6-external-services" class="md-nav__link">
<span class="md-ellipsis">
6. External Services
</span>
</a>
<nav class="md-nav" aria-label="6. External Services">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#smtp-server" class="md-nav__link">
<span class="md-ellipsis">
SMTP Server
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#represent-api" class="md-nav__link">
<span class="md-ellipsis">
Represent API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#geocoding-providers" class="md-nav__link">
<span class="md-ellipsis">
Geocoding Providers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#listmonk-newsletter-platform" class="md-nav__link">
<span class="md-ellipsis">
Listmonk Newsletter Platform
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#7-observability-stack" class="md-nav__link">
<span class="md-ellipsis">
7. Observability Stack
</span>
</a>
<nav class="md-nav" aria-label="7. Observability Stack">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#prometheus" class="md-nav__link">
<span class="md-ellipsis">
Prometheus
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#grafana" class="md-nav__link">
<span class="md-ellipsis">
Grafana
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#alertmanager" class="md-nav__link">
<span class="md-ellipsis">
Alertmanager
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#request-lifecycle" class="md-nav__link">
<span class="md-ellipsis">
Request Lifecycle
</span>
</a>
<nav class="md-nav" aria-label="Request Lifecycle">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#example-public-campaign-email-submission" class="md-nav__link">
<span class="md-ellipsis">
Example: Public Campaign Email Submission
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#technology-decisions" class="md-nav__link">
<span class="md-ellipsis">
Technology Decisions
</span>
</a>
<nav class="md-nav" aria-label="Technology Decisions">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#why-typescript" class="md-nav__link">
<span class="md-ellipsis">
Why TypeScript?
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#why-prisma-drizzle" class="md-nav__link">
<span class="md-ellipsis">
Why Prisma + Drizzle?
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#why-dual-api" class="md-nav__link">
<span class="md-ellipsis">
Why Dual API?
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#why-jwt-over-sessions" class="md-nav__link">
<span class="md-ellipsis">
Why JWT over Sessions?
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#why-bullmq-over-bull" class="md-nav__link">
<span class="md-ellipsis">
Why BullMQ over Bull?
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#why-postgresql-over-nosql" class="md-nav__link">
<span class="md-ellipsis">
Why PostgreSQL over NoSQL?
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#deployment-architecture" class="md-nav__link">
<span class="md-ellipsis">
Deployment Architecture
</span>
</a>
<nav class="md-nav" aria-label="Deployment Architecture">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#docker-compose" class="md-nav__link">
<span class="md-ellipsis">
Docker Compose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#nginx-routing" class="md-nav__link">
<span class="md-ellipsis">
Nginx Routing
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#security-architecture" class="md-nav__link">
<span class="md-ellipsis">
Security Architecture
</span>
</a>
<nav class="md-nav" aria-label="Security Architecture">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#authentication-flow" class="md-nav__link">
<span class="md-ellipsis">
Authentication Flow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#security-layers" class="md-nav__link">
<span class="md-ellipsis">
Security Layers
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#scalability-considerations" class="md-nav__link">
<span class="md-ellipsis">
Scalability Considerations
</span>
</a>
<nav class="md-nav" aria-label="Scalability Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#horizontal-scaling" class="md-nav__link">
<span class="md-ellipsis">
Horizontal Scaling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#vertical-scaling" class="md-nav__link">
<span class="md-ellipsis">
Vertical Scaling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#bottlenecks" class="md-nav__link">
<span class="md-ellipsis">
Bottlenecks
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#monitoring-observability" class="md-nav__link">
<span class="md-ellipsis">
Monitoring &amp; Observability
</span>
</a>
<nav class="md-nav" aria-label="Monitoring &amp; Observability">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#golden-signals" class="md-nav__link">
<span class="md-ellipsis">
Golden Signals
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#slos-service-level-objectives" class="md-nav__link">
<span class="md-ellipsis">
SLOs (Service Level Objectives)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#alerting-strategy" class="md-nav__link">
<span class="md-ellipsis">
Alerting Strategy
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#further-reading" class="md-nav__link">
<span class="md-ellipsis">
Further Reading
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<nav class="md-path" aria-label="Navigation" >
<ol class="md-path__list">
<li class="md-path__item">
<a href="../.." class="md-path__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-path__item">
<a href="../" class="md-path__link">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
</li>
<li class="md-path__item">
<a href="./" class="md-path__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
</ol>
</nav>
<article class="md-content__inner md-typeset">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/architecture/index.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/architecture/index.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
<h1 id="v2-architecture-overview">V2 Architecture Overview<a class="headerlink" href="#v2-architecture-overview" title="Permanent link">&para;</a></h1>
<p>Changemaker Lite V2 is built on a modern microservices architecture with a dual API design, React admin interface, and comprehensive observability.</p>
<h2 id="system-architecture">System Architecture<a class="headerlink" href="#system-architecture" title="Permanent link">&para;</a></h2>
<pre class="mermaid"><code>graph TB
subgraph "User Access"
Browser[Web Browser]
VolunteerApp[Volunteer Mobile]
end
subgraph "Nginx Reverse Proxy"
Nginx[Nginx&lt;br/&gt;Subdomain Router]
end
subgraph "Frontend Layer"
AdminGUI[Admin GUI&lt;br/&gt;React + Vite + Ant Design&lt;br/&gt;Port 3000]
PublicPages[Public Pages&lt;br/&gt;Dark Theme]
VolunteerPortal[Volunteer Portal&lt;br/&gt;GPS Canvassing]
end
subgraph "Backend Layer - Dual API"
ExpressAPI[Express API&lt;br/&gt;Main Features&lt;br/&gt;Port 4000&lt;br/&gt;Prisma ORM]
FastifyAPI[Fastify API&lt;br/&gt;Media Library&lt;br/&gt;Port 4100&lt;br/&gt;Drizzle ORM]
end
subgraph "Data Layer"
Postgres[(PostgreSQL 16&lt;br/&gt;27+ Models)]
Redis[(Redis&lt;br/&gt;Cache + Queues)]
end
subgraph "Job Processing"
EmailQueue[BullMQ&lt;br/&gt;Email Queue]
GeocodeQueue[BullMQ&lt;br/&gt;Geocoding Queue]
end
subgraph "External Services"
SMTP[SMTP Server&lt;br/&gt;Email Delivery]
Represent[Represent API&lt;br/&gt;Canadian Reps]
Geocoding[Geocoding Providers&lt;br/&gt;6 Services]
Listmonk[Listmonk&lt;br/&gt;Newsletter Platform]
end
subgraph "Observability"
Prometheus[Prometheus&lt;br/&gt;Metrics]
Grafana[Grafana&lt;br/&gt;Dashboards]
Alertmanager[Alertmanager&lt;br/&gt;Notifications]
end
Browser --&gt; Nginx
VolunteerApp --&gt; Nginx
Nginx --&gt; AdminGUI
Nginx --&gt; PublicPages
Nginx --&gt; VolunteerPortal
AdminGUI --&gt; ExpressAPI
AdminGUI --&gt; FastifyAPI
PublicPages --&gt; ExpressAPI
VolunteerPortal --&gt; ExpressAPI
ExpressAPI --&gt; Postgres
ExpressAPI --&gt; Redis
ExpressAPI --&gt; EmailQueue
ExpressAPI --&gt; GeocodeQueue
ExpressAPI --&gt; Represent
ExpressAPI --&gt; Geocoding
ExpressAPI --&gt; Listmonk
ExpressAPI --&gt; Prometheus
FastifyAPI --&gt; Postgres
FastifyAPI --&gt; Redis
FastifyAPI --&gt; Prometheus
EmailQueue --&gt; Redis
EmailQueue --&gt; SMTP
GeocodeQueue --&gt; Redis
GeocodeQueue --&gt; Geocoding
Prometheus --&gt; Grafana
Prometheus --&gt; Alertmanager</code></pre>
<h2 id="core-components">Core Components<a class="headerlink" href="#core-components" title="Permanent link">&para;</a></h2>
<h3 id="1-nginx-reverse-proxy">1. Nginx Reverse Proxy<a class="headerlink" href="#1-nginx-reverse-proxy" title="Permanent link">&para;</a></h3>
<p><strong>Purpose</strong>: Routes HTTP requests to appropriate services based on subdomain</p>
<p><strong>Subdomains</strong>:
- <code>app.cmlite.org</code> → Admin GUI (React)
- <code>api.cmlite.org</code> → Express API (main features)
- <code>media.cmlite.org</code> → Fastify API (video library)
- <code>db.cmlite.org</code> → NocoDB (data browser)
- <code>docs.cmlite.org</code> → MkDocs (documentation)
- <code>listmonk.cmlite.org</code> → Listmonk (newsletter)
- <code>grafana.cmlite.org</code> → Grafana (monitoring)
- And 8 more service subdomains...</p>
<p><strong>Configuration</strong>: <code>/nginx/conf.d/</code></p>
<p><a href="networking.md">Learn more →</a></p>
<h3 id="2-frontend-layer">2. Frontend Layer<a class="headerlink" href="#2-frontend-layer" title="Permanent link">&para;</a></h3>
<h4 id="admin-gui-port-3000">Admin GUI (Port 3000)<a class="headerlink" href="#admin-gui-port-3000" title="Permanent link">&para;</a></h4>
<ul>
<li><strong>Framework</strong>: React 19 with Vite build tool</li>
<li><strong>UI Library</strong>: Ant Design 5 (Table, Form, Modal, Drawer components)</li>
<li><strong>State Management</strong>: Zustand stores (auth, canvass)</li>
<li><strong>Routing</strong>: React Router v6</li>
<li><strong>HTTP Client</strong>: Axios with 401 refresh interceptor</li>
</ul>
<p><strong>Structure</strong>:
- 32 admin pages (campaigns, locations, users, settings, etc.)
- 6 public pages (campaign view, response wall, map, shifts)
- 4 volunteer portal pages (canvassing, assignments, activity)
- Shared components (map, canvass, GrapesJS editor)</p>
<p><a href="frontend.md">Learn more →</a></p>
<h4 id="public-pages">Public Pages<a class="headerlink" href="#public-pages" title="Permanent link">&para;</a></h4>
<ul>
<li>Dark blue/teal theme (consistent with V1 branding)</li>
<li>No authentication required</li>
<li>Mobile-responsive layouts</li>
<li>Public campaign submission</li>
<li>Response wall with upvoting</li>
<li>Public map with location markers</li>
<li>Shift signup forms</li>
</ul>
<h4 id="volunteer-portal">Volunteer Portal<a class="headerlink" href="#volunteer-portal" title="Permanent link">&para;</a></h4>
<ul>
<li>Top navigation layout</li>
<li>Mobile-optimized (hamburger menu)</li>
<li>GPS-tracked canvassing</li>
<li>Full-screen map interface</li>
<li>Visit recording forms</li>
<li>Activity tracking</li>
</ul>
<h3 id="3-backend-layer-dual-api-design">3. Backend Layer - Dual API Design<a class="headerlink" href="#3-backend-layer-dual-api-design" title="Permanent link">&para;</a></h3>
<h4 id="express-api-port-4000">Express API (Port 4000)<a class="headerlink" href="#express-api-port-4000" title="Permanent link">&para;</a></h4>
<p><strong>Main application server</strong> handling core features:</p>
<p><strong>14 Feature Modules</strong>:
1. <strong>auth</strong> - JWT login, register, refresh, logout
2. <strong>users</strong> - User CRUD with pagination
3. <strong>settings</strong> - Site settings singleton
4. <strong>campaigns</strong> - Campaign CRUD + public routes
5. <strong>representatives</strong> - Represent API integration
6. <strong>responses</strong> - Response wall + moderation
7. <strong>email-queue</strong> - BullMQ queue admin
8. <strong>campaign-emails</strong> - Email tracking + stats
9. <strong>postal-codes</strong> - Postal code cache
10. <strong>locations</strong> - Location CRUD + geocoding + NAR import
11. <strong>cuts</strong> - Cut (polygon) CRUD + spatial queries
12. <strong>shifts</strong> - Shift CRUD + signups
13. <strong>canvass</strong> - Volunteer canvassing (sessions, visits, routes)
14. <strong>pages</strong> - Landing page builder (GrapesJS)</p>
<p><strong>Plus</strong>: email-templates, listmonk, pangolin, docs, qr, services, observability</p>
<p><strong>ORM</strong>: Prisma (27+ models)</p>
<p><strong>Architecture</strong>:
- Layered structure (routes → services → database)
- Zod schema validation
- Role-based access control (RBAC)
- Error handling middleware
- Winston logging</p>
<p><a href="dual-api/">Learn more →</a></p>
<h4 id="fastify-api-port-4100">Fastify API (Port 4100)<a class="headerlink" href="#fastify-api-port-4100" title="Permanent link">&para;</a></h4>
<p><strong>Specialized microservice</strong> for media library:</p>
<p><strong>Features</strong>:
- Video CRUD (title, duration, orientation, producer)
- Shared media (public gallery categories)
- Lock/unlock system (public visibility control)
- Reaction system (6 standard emojis)
- Job queue monitoring
- Bulk operations</p>
<p><strong>ORM</strong>: Drizzle (lightweight schema-first)</p>
<p><strong>Why Separate?</strong>:
- Performance isolation (video ops don't slow main API)
- Different ORM evaluation (Drizzle vs Prisma)
- Independent scaling
- Clear service boundaries</p>
<p><strong>Shared Resources</strong>:
- Same PostgreSQL database (different schemas)
- Same Redis instance
- Reuses JWT_ACCESS_SECRET for auth</p>
<p><a href="dual-api/">Learn more →</a></p>
<h3 id="4-data-layer">4. Data Layer<a class="headerlink" href="#4-data-layer" title="Permanent link">&para;</a></h3>
<h4 id="postgresql-16">PostgreSQL 16<a class="headerlink" href="#postgresql-16" title="Permanent link">&para;</a></h4>
<p><strong>Primary database</strong> with two ORM schemas:</p>
<p><strong>Prisma Schema</strong> (27+ models):
- User, RefreshToken (auth)
- Campaign, Representative, Response, CampaignEmail (influence)
- Location, Cut, Shift, ShiftSignup (map)
- CanvassSession, CanvassVisit, TrackingSession, TrackPoint (canvass)
- LandingPage, PageBlock, EmailTemplate (content)
- SiteSettings, MapSettings (config)</p>
<p><strong>Drizzle Schema</strong> (media tables):
- videos
- shared_media
- reactions
- jobs</p>
<p><strong>Indexes</strong>: Optimized for common queries (userId, campaignId, cutId, etc.)</p>
<p><a href="database.md">Learn more →</a></p>
<h4 id="redis">Redis<a class="headerlink" href="#redis" title="Permanent link">&para;</a></h4>
<p><strong>Multi-purpose cache and queue backend</strong>:</p>
<ul>
<li><strong>Caching</strong>: Postal codes (7-day TTL), representatives</li>
<li><strong>Rate Limiting</strong>: Per-endpoint limits (Redis-backed)</li>
<li><strong>BullMQ Queues</strong>: Email sending, bulk geocoding</li>
<li><strong>Sessions</strong>: Future session storage (if needed)</li>
</ul>
<p><strong>Authentication</strong>: Required (<code>REDIS_PASSWORD</code> env var)</p>
<h3 id="5-job-processing">5. Job Processing<a class="headerlink" href="#5-job-processing" title="Permanent link">&para;</a></h3>
<h4 id="bullmq-queues">BullMQ Queues<a class="headerlink" href="#bullmq-queues" title="Permanent link">&para;</a></h4>
<p><strong>Async job processing</strong> for long-running operations:</p>
<p><strong>Email Queue</strong>:
- Campaign email sending (SMTP)
- Email verification (double opt-in)
- Confirmation emails (shift signups)
- Retry logic (exponential backoff)
- Rate limiting (avoid spam flagging)</p>
<p><strong>Geocoding Queue</strong>:
- Bulk address geocoding
- Multi-provider fallback (6 services)
- Rate limit compliance (500 jobs/min)
- Result caching</p>
<p><strong>Queue Management</strong>:
- Admin routes for pause/resume
- Job status monitoring
- Failed job inspection
- Queue metrics (Prometheus)</p>
<h3 id="6-external-services">6. External Services<a class="headerlink" href="#6-external-services" title="Permanent link">&para;</a></h3>
<h4 id="smtp-server">SMTP Server<a class="headerlink" href="#smtp-server" title="Permanent link">&para;</a></h4>
<p>Email delivery for:
- Campaign advocacy emails
- Email verification
- Password reset
- Shift confirmation
- Admin notifications</p>
<p><strong>Dev Mode</strong>: MailHog captures emails (<code>EMAIL_TEST_MODE=true</code>)</p>
<h4 id="represent-api">Represent API<a class="headerlink" href="#represent-api" title="Permanent link">&para;</a></h4>
<p>Canadian elected representative lookup:
- Postal code → MPs, MPPs, councillors
- Caching (7-day TTL per postal code)
- Fallback to cached data on API errors</p>
<h4 id="geocoding-providers">Geocoding Providers<a class="headerlink" href="#geocoding-providers" title="Permanent link">&para;</a></h4>
<p>Multi-provider geocoding with fallback:</p>
<ol>
<li>Nominatim (OpenStreetMap, free)</li>
<li>Mapbox (requires API key, best accuracy)</li>
<li>ArcGIS (free tier available)</li>
<li>Photon (OSM-based, no key required)</li>
<li>Google (requires API key, high cost)</li>
<li>LocationIQ (requires API key, generous free tier)</li>
</ol>
<p><strong>Strategy</strong>: Try each provider in order until success</p>
<h4 id="listmonk-newsletter-platform">Listmonk Newsletter Platform<a class="headerlink" href="#listmonk-newsletter-platform" title="Permanent link">&para;</a></h4>
<p>Email marketing integration:
- Sync participants/locations/users → subscriber lists
- Newsletter campaigns (separate from advocacy emails)
- API integration (basic auth)
- Health monitoring</p>
<h3 id="7-observability-stack">7. Observability Stack<a class="headerlink" href="#7-observability-stack" title="Permanent link">&para;</a></h3>
<h4 id="prometheus">Prometheus<a class="headerlink" href="#prometheus" title="Permanent link">&para;</a></h4>
<p><strong>Metrics collection</strong> with custom instrumentation:</p>
<p><strong>12 Custom Metrics</strong> (<code>cm_*</code> prefix):
- <code>cm_api_uptime_seconds</code> - API availability
- <code>cm_email_queue_size</code> - Queue depth
- <code>cm_email_sent_total</code> - Email delivery count
- <code>cm_geocode_success_rate</code> - Geocoding quality
- <code>cm_active_canvass_sessions</code> - Live canvassing
- And 7 more domain-specific metrics...</p>
<p><strong>HTTP Metrics</strong>:
- <code>http_request_total</code> - Total requests
- <code>http_request_duration_seconds</code> - Latency histogram
- <code>http_request_errors_total</code> - Error count</p>
<p><strong>Scrape Targets</strong>:
- Express API (<code>:4000/metrics</code>)
- Fastify API (<code>:4100/metrics</code>)
- Redis Exporter
- Node Exporter (host metrics)
- cAdvisor (container metrics)</p>
<p><a href="monitoring.md">Learn more →</a></p>
<h4 id="grafana">Grafana<a class="headerlink" href="#grafana" title="Permanent link">&para;</a></h4>
<p><strong>Visualization dashboards</strong>:</p>
<ol>
<li><strong>Application Overview</strong> - API metrics, queue stats, sessions</li>
<li><strong>Infrastructure</strong> - Container metrics, host resources, Redis</li>
<li><strong>Alerts &amp; SLOs</strong> - Error budgets, SLI tracking</li>
</ol>
<p><strong>Auto-provisioned</strong>: Dashboards in <code>/configs/grafana/</code></p>
<h4 id="alertmanager">Alertmanager<a class="headerlink" href="#alertmanager" title="Permanent link">&para;</a></h4>
<p><strong>Alert routing and notifications</strong>:</p>
<p><strong>12 Alert Rules</strong>:
- High error rate (&gt;5% for 5 minutes)
- Email queue stuck (no jobs processed in 10 minutes)
- Service down (health check fails)
- Database connection pool exhausted
- Redis unavailable
- And 7 more critical conditions...</p>
<p><strong>Notification Channels</strong>:
- Gotify (self-hosted push notifications)
- Email (SMTP)
- Webhook (custom integrations)</p>
<h2 id="request-lifecycle">Request Lifecycle<a class="headerlink" href="#request-lifecycle" title="Permanent link">&para;</a></h2>
<h3 id="example-public-campaign-email-submission">Example: Public Campaign Email Submission<a class="headerlink" href="#example-public-campaign-email-submission" title="Permanent link">&para;</a></h3>
<pre class="mermaid"><code>sequenceDiagram
participant User as User Browser
participant Nginx
participant Admin as Admin GUI
participant Express as Express API
participant DB as PostgreSQL
participant Redis
participant Queue as BullMQ
participant SMTP as SMTP Server
participant Rep as Represent API
User-&gt;&gt;Nginx: Visit /campaigns/123
Nginx-&gt;&gt;Admin: Route to React app
Admin-&gt;&gt;Express: GET /api/campaigns/123 (public)
Express-&gt;&gt;DB: Query campaign
DB--&gt;&gt;Express: Campaign data
Express--&gt;&gt;Admin: Campaign JSON
Admin--&gt;&gt;User: Render campaign page
User-&gt;&gt;Admin: Enter postal code + submit
Admin-&gt;&gt;Express: POST /api/postal-codes (lookup)
Express-&gt;&gt;Redis: Check cache
Redis--&gt;&gt;Express: Cache miss
Express-&gt;&gt;Rep: GET /representatives/postal-code
Rep--&gt;&gt;Express: Representative list
Express-&gt;&gt;Redis: Cache for 7 days
Express--&gt;&gt;Admin: Representatives JSON
Admin--&gt;&gt;User: Show rep selection
User-&gt;&gt;Admin: Select rep + write email + submit
Admin-&gt;&gt;Express: POST /api/responses (create)
Express-&gt;&gt;DB: Insert response
Express-&gt;&gt;Queue: Enqueue verification email
Express-&gt;&gt;DB: Insert campaign email record
DB--&gt;&gt;Express: Response created
Express--&gt;&gt;Admin: Success response
Admin--&gt;&gt;User: Show success message
Queue-&gt;&gt;SMTP: Send verification email
SMTP--&gt;&gt;Queue: Delivery confirmed
User-&gt;&gt;User: Click verification link (email)
User-&gt;&gt;Nginx: GET /verify-response/:token
Nginx-&gt;&gt;Admin: Route to React app
Admin-&gt;&gt;Express: POST /api/responses/:id/verify
Express-&gt;&gt;DB: Update response (verified=true)
Express-&gt;&gt;Queue: Enqueue campaign email to rep
DB--&gt;&gt;Express: Response verified
Express--&gt;&gt;Admin: Success
Admin--&gt;&gt;User: Email sent confirmation
Queue-&gt;&gt;SMTP: Send campaign email to rep
SMTP--&gt;&gt;Queue: Delivery confirmed</code></pre>
<h2 id="technology-decisions">Technology Decisions<a class="headerlink" href="#technology-decisions" title="Permanent link">&para;</a></h2>
<h3 id="why-typescript">Why TypeScript?<a class="headerlink" href="#why-typescript" title="Permanent link">&para;</a></h3>
<ul>
<li>Type safety reduces runtime errors</li>
<li>Better IDE support and autocomplete</li>
<li>Easier refactoring</li>
<li>Self-documenting code</li>
</ul>
<h3 id="why-prisma-drizzle">Why Prisma + Drizzle?<a class="headerlink" href="#why-prisma-drizzle" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Prisma</strong>: Great for complex models, migrations, auto-generated types</li>
<li><strong>Drizzle</strong>: Lightweight, perfect for simple media tables</li>
<li>Evaluate both ORMs in production</li>
</ul>
<h3 id="why-dual-api">Why Dual API?<a class="headerlink" href="#why-dual-api" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Separation of concerns</strong>: Media ops isolated from core features</li>
<li><strong>Performance</strong>: Video processing doesn't block main API</li>
<li><strong>Scalability</strong>: Independent horizontal scaling</li>
<li><strong>Technology evaluation</strong>: Compare Express vs Fastify</li>
</ul>
<h3 id="why-jwt-over-sessions">Why JWT over Sessions?<a class="headerlink" href="#why-jwt-over-sessions" title="Permanent link">&para;</a></h3>
<ul>
<li>Stateless (scales horizontally)</li>
<li>No session storage overhead</li>
<li>Works across multiple API servers</li>
<li>Standard claims (iat, exp, sub)</li>
</ul>
<h3 id="why-bullmq-over-bull">Why BullMQ over Bull?<a class="headerlink" href="#why-bullmq-over-bull" title="Permanent link">&para;</a></h3>
<ul>
<li>Better TypeScript support</li>
<li>Improved performance</li>
<li>Active maintenance</li>
<li>Better documentation</li>
</ul>
<h3 id="why-postgresql-over-nosql">Why PostgreSQL over NoSQL?<a class="headerlink" href="#why-postgresql-over-nosql" title="Permanent link">&para;</a></h3>
<ul>
<li>Complex relational data (campaigns, locations, users)</li>
<li>ACID transactions (critical for email queue)</li>
<li>Full-text search</li>
<li>Spatial queries (PostGIS for future geo features)</li>
</ul>
<h2 id="deployment-architecture">Deployment Architecture<a class="headerlink" href="#deployment-architecture" title="Permanent link">&para;</a></h2>
<h3 id="docker-compose">Docker Compose<a class="headerlink" href="#docker-compose" title="Permanent link">&para;</a></h3>
<p>All services orchestrated in <code>docker-compose.yml</code>:</p>
<p><strong>Profiles</strong>:
- <code>default</code>: Core services (postgres, redis, api, admin, nginx)
- <code>monitoring</code>: Prometheus, Grafana, Alertmanager, exporters</p>
<p><strong>Networks</strong>:
- <code>changemaker-lite</code> bridge network
- Service discovery via container names</p>
<p><strong>Volumes</strong>:
- PostgreSQL data persistence
- Redis data persistence
- Uploads directory
- Logs directory</p>
<p><a href="../deployment/docker-compose/">Learn more →</a></p>
<h3 id="nginx-routing">Nginx Routing<a class="headerlink" href="#nginx-routing" title="Permanent link">&para;</a></h3>
<p><strong>Subdomain-based routing</strong>:</p>
<div class="language-nginx highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="c1"># Admin GUI</span>
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a><span class="k">server</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a><span class="w"> </span><span class="kn">server_name</span><span class="w"> </span><span class="s">app.cmlite.org</span><span class="p">;</span>
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a><span class="w"> </span><span class="kn">location</span><span class="w"> </span><span class="s">/</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a><span class="w"> </span><span class="kn">proxy_pass</span><span class="w"> </span><span class="s">http://admin:3000</span><span class="p">;</span>
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a><span class="p">}</span>
</span><span id="__span-0-8"><a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a>
</span><span id="__span-0-9"><a id="__codelineno-0-9" name="__codelineno-0-9" href="#__codelineno-0-9"></a><span class="c1"># Express API</span>
</span><span id="__span-0-10"><a id="__codelineno-0-10" name="__codelineno-0-10" href="#__codelineno-0-10"></a><span class="k">server</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-0-11"><a id="__codelineno-0-11" name="__codelineno-0-11" href="#__codelineno-0-11"></a><span class="w"> </span><span class="kn">server_name</span><span class="w"> </span><span class="s">api.cmlite.org</span><span class="p">;</span>
</span><span id="__span-0-12"><a id="__codelineno-0-12" name="__codelineno-0-12" href="#__codelineno-0-12"></a><span class="w"> </span><span class="kn">location</span><span class="w"> </span><span class="s">/</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-0-13"><a id="__codelineno-0-13" name="__codelineno-0-13" href="#__codelineno-0-13"></a><span class="w"> </span><span class="kn">proxy_pass</span><span class="w"> </span><span class="s">http://api:4000</span><span class="p">;</span>
</span><span id="__span-0-14"><a id="__codelineno-0-14" name="__codelineno-0-14" href="#__codelineno-0-14"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-0-15"><a id="__codelineno-0-15" name="__codelineno-0-15" href="#__codelineno-0-15"></a><span class="p">}</span>
</span><span id="__span-0-16"><a id="__codelineno-0-16" name="__codelineno-0-16" href="#__codelineno-0-16"></a>
</span><span id="__span-0-17"><a id="__codelineno-0-17" name="__codelineno-0-17" href="#__codelineno-0-17"></a><span class="c1"># Fastify Media API</span>
</span><span id="__span-0-18"><a id="__codelineno-0-18" name="__codelineno-0-18" href="#__codelineno-0-18"></a><span class="k">server</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-0-19"><a id="__codelineno-0-19" name="__codelineno-0-19" href="#__codelineno-0-19"></a><span class="w"> </span><span class="kn">server_name</span><span class="w"> </span><span class="s">media.cmlite.org</span><span class="p">;</span>
</span><span id="__span-0-20"><a id="__codelineno-0-20" name="__codelineno-0-20" href="#__codelineno-0-20"></a><span class="w"> </span><span class="kn">location</span><span class="w"> </span><span class="s">/</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-0-21"><a id="__codelineno-0-21" name="__codelineno-0-21" href="#__codelineno-0-21"></a><span class="w"> </span><span class="kn">proxy_pass</span><span class="w"> </span><span class="s">http://media-api:4100</span><span class="p">;</span>
</span><span id="__span-0-22"><a id="__codelineno-0-22" name="__codelineno-0-22" href="#__codelineno-0-22"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-0-23"><a id="__codelineno-0-23" name="__codelineno-0-23" href="#__codelineno-0-23"></a><span class="p">}</span>
</span></code></pre></div>
<p><a href="networking.md">Learn more →</a></p>
<h2 id="security-architecture">Security Architecture<a class="headerlink" href="#security-architecture" title="Permanent link">&para;</a></h2>
<h3 id="authentication-flow">Authentication Flow<a class="headerlink" href="#authentication-flow" title="Permanent link">&para;</a></h3>
<pre class="mermaid"><code>sequenceDiagram
participant Client
participant API as Express API
participant DB as PostgreSQL
participant Redis
Client-&gt;&gt;API: POST /api/auth/login
API-&gt;&gt;DB: Verify credentials
DB--&gt;&gt;API: User record
API-&gt;&gt;DB: Create refresh token (expires 7d)
API-&gt;&gt;Redis: Rate limit check
API--&gt;&gt;Client: Access token (15min) + Refresh token (7d)
Note over Client: Access token expires
Client-&gt;&gt;API: POST /api/auth/refresh
API-&gt;&gt;DB: Validate refresh token
DB--&gt;&gt;API: Token valid
API-&gt;&gt;DB: Rotate refresh token (transaction)
API--&gt;&gt;Client: New access token + New refresh token</code></pre>
<p><strong>Features</strong>:
- bcrypt password hashing (12+ chars, complexity requirements)
- JWT access tokens (15min expiry)
- Refresh tokens (7 days, stored in DB, rotated on use)
- Rate limiting (10 requests/min on auth endpoints)
- User enumeration prevention (401 not 404)
- RBAC middleware (requireRole, requireNonTemp)</p>
<p><a href="authentication/">Learn more →</a></p>
<h3 id="security-layers">Security Layers<a class="headerlink" href="#security-layers" title="Permanent link">&para;</a></h3>
<ol>
<li><strong>Network</strong>: Nginx rate limiting, fail2ban</li>
<li><strong>Application</strong>: Input validation (Zod schemas), RBAC</li>
<li><strong>Data</strong>: Encrypted fields (ENCRYPTION_KEY), SQL injection prevention (Prisma)</li>
<li><strong>Transport</strong>: HTTPS only (production), HSTS headers</li>
</ol>
<p><a href="security.md">Learn more →</a></p>
<h2 id="scalability-considerations">Scalability Considerations<a class="headerlink" href="#scalability-considerations" title="Permanent link">&para;</a></h2>
<h3 id="horizontal-scaling">Horizontal Scaling<a class="headerlink" href="#horizontal-scaling" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Stateless APIs</strong>: JWT auth allows multiple API instances</li>
<li><strong>Redis-backed queues</strong>: Share job queues across workers</li>
<li><strong>Database connection pooling</strong>: Prisma manages connections</li>
<li><strong>Nginx load balancing</strong>: Distribute requests across API instances</li>
</ul>
<h3 id="vertical-scaling">Vertical Scaling<a class="headerlink" href="#vertical-scaling" title="Permanent link">&para;</a></h3>
<ul>
<li>Increase container resources (CPU, memory)</li>
<li>Optimize database queries (indexes, query planning)</li>
<li>Redis memory limits (LRU eviction policy)</li>
</ul>
<h3 id="bottlenecks">Bottlenecks<a class="headerlink" href="#bottlenecks" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>PostgreSQL</strong>: Single primary (future: read replicas)</li>
<li><strong>Redis</strong>: Single instance (future: Redis Cluster)</li>
<li><strong>File uploads</strong>: Local disk (future: S3-compatible storage)</li>
</ul>
<h2 id="monitoring-observability">Monitoring &amp; Observability<a class="headerlink" href="#monitoring-observability" title="Permanent link">&para;</a></h2>
<h3 id="golden-signals">Golden Signals<a class="headerlink" href="#golden-signals" title="Permanent link">&para;</a></h3>
<ol>
<li><strong>Latency</strong>: Request duration histograms</li>
<li><strong>Traffic</strong>: Request rate by endpoint</li>
<li><strong>Errors</strong>: Error rate (5xx responses)</li>
<li><strong>Saturation</strong>: Database connections, Redis memory, queue depth</li>
</ol>
<h3 id="slos-service-level-objectives">SLOs (Service Level Objectives)<a class="headerlink" href="#slos-service-level-objectives" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Availability</strong>: 99.9% uptime (8.76 hours downtime/year)</li>
<li><strong>Latency</strong>: p95 &lt; 500ms, p99 &lt; 1000ms</li>
<li><strong>Error Rate</strong>: &lt; 0.1% (1 error per 1000 requests)</li>
</ul>
<h3 id="alerting-strategy">Alerting Strategy<a class="headerlink" href="#alerting-strategy" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Critical</strong>: Page on-call (service down, database unavailable)</li>
<li><strong>Warning</strong>: Create ticket (queue growing, elevated errors)</li>
<li><strong>Info</strong>: Log only (slow query, cache miss)</li>
</ul>
<p><a href="monitoring.md">Learn more →</a></p>
<h2 id="further-reading">Further Reading<a class="headerlink" href="#further-reading" title="Permanent link">&para;</a></h2>
<ul>
<li><a href="dual-api/">Dual API Architecture</a> - Express + Fastify design</li>
<li><a href="database.md">Database Schema</a> - Complete ER diagram</li>
<li><a href="authentication/">Authentication Flow</a> - JWT security model</li>
<li><a href="frontend.md">Frontend Architecture</a> - React + Vite + Ant Design</li>
<li><a href="networking.md">Networking</a> - Nginx routing and subdomains</li>
<li><a href="security.md">Security Model</a> - Comprehensive security audit</li>
<li><a href="monitoring.md">Monitoring Stack</a> - Prometheus + Grafana + Alertmanager</li>
<li><a href="data-flow.md">Data Flow</a> - Request lifecycle examples</li>
</ul>
<hr />
<p><strong>Next</strong>: <a href="../development/local-setup/">Set up your development environment →</a></p>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../getting-started/quick-start/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Quick Start">
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</div>
<div class="md-footer__title">
<span class="md-footer__direction">
Previous
</span>
<div class="md-ellipsis">
Quick Start
</div>
</div>
</a>
<a href="dual-api/" class="md-footer__link md-footer__link--next" aria-label="Next: Dual API System">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Dual API System
</div>
</div>
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg>
</div>
</a>
</nav>
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
<div class="md-copyright__highlight">
Copyright &copy; 2024 The Bunker Operations <a href="#__consent">Change cookie settings</a>
</div>
</div>
<div class="md-social">
<a href="https://gitea.bnkops.com/admin" target="_blank" rel="noopener" title="Gitea Repository" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M252.8 8C114.1 8 8 113.3 8 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C436.2 457.8 504 362.9 504 252 504 113.3 391.5 8 252.8 8M105.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></svg>
</a>
<a href="https://listmonk.bnkops.com/subscription/form" target="_blank" rel="noopener" title="Newsletter" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M536.4-26.3c9.8-3.5 20.6-1 28 6.3s9.8 18.2 6.3 28l-178 496.9c-5 13.9-18.1 23.1-32.8 23.1-14.2 0-27-8.6-32.3-21.7l-64.2-158c-4.5-11-2.5-23.6 5.2-32.6l94.5-112.4c5.1-6.1 4.7-15-.9-20.6s-14.6-6-20.6-.9l-112.4 94.3c-9.1 7.6-21.6 9.6-32.6 5.2L38.1 216.8c-13.1-5.3-21.7-18.1-21.7-32.3 0-14.7 9.2-27.8 23.1-32.8z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../..", "features": ["announce.dismiss", "content.action.edit", "content.action.view", "content.code.annotate", "content.code.copy", "content.tooltips", "navigation.expand", "navigation.footer", "navigation.indexes", "navigation.path", "navigation.prune", "navigation.sections", "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "search": "../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../assets/javascripts/bundle.79ae519e.min.js"></script>
<script src="../../javascripts/home.js"></script>
<script src="../../javascripts/github-widget.js"></script>
<script src="../../javascripts/gitea-widget.js"></script>
<script src="../../assets/js/env-config.js"></script>
<script src="../../assets/js/video-player.js"></script>
</body>
</html>