|
| 1 | +# High-Level Architecture |
| 2 | + |
| 3 | +This section describes the different components of {{extra.project}}, their roles, and how they interact. |
| 4 | +It is important to understand that the architecture is intentionally designed to avoid a monolithic structure—that is, bundling everything into a single piece. |
| 5 | + |
| 6 | +In this regard, {{extra.project}} adheres to the proven Unix philosophy: |
| 7 | +Build small, focused parts with clear interfaces, and combine them as needed. |
| 8 | + |
| 9 | +Please note: Ignore technical details for now and focus on the overall structure. |
| 10 | +This section is not intended as a setup guide for your development environment, but rather as a high-level architectural overview. |
| 11 | + |
| 12 | +## The Core Part 1 – REST API |
| 13 | + |
| 14 | +If you have `git`, `python`, and the `poetry` package manager installed on your computer, try the following: |
| 15 | + |
| 16 | +1. `git clone https://github.com/papermerge/papermerge-core` |
| 17 | +2. `cd papermerge-core/` |
| 18 | +3. `poetry install` |
| 19 | +4. `poetry run task server` |
| 20 | + |
| 21 | +The last command attempts to start the REST API server on port **8000**. |
| 22 | + |
| 23 | +Well, almost—it's likely to throw some errors if you don’t have a database configured yet. |
| 24 | +For example, you might not have PostgreSQL running, or the required environment variables might be missing. |
| 25 | + |
| 26 | +However, **assuming** you have: |
| 27 | + |
| 28 | +* PostgreSQL up and running, |
| 29 | +* a database already created, |
| 30 | +* and the correct environment variables (such as `PAPERMERGE__DATABASE__URL`) set, |
| 31 | + |
| 32 | +...then the REST API server should start successfully. |
| 33 | + |
| 34 | +The illustration below shows a basic REST API server waiting for HTTP requests on port **8000**: |
| 35 | + |
| 36 | + |
| 37 | + |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## Two Very Important Points |
| 42 | + |
| 43 | +There are two key things to understand about the Core REST API server: |
| 44 | + |
| 45 | +1. **There is no UI** (i.e. no frontend) |
| 46 | +2. **There is no authentication** |
| 47 | + |
| 48 | +Let’s unpack both points. |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +### 1. No UI |
| 53 | + |
| 54 | +I assume you already know what a REST API is. Personally, I find it intuitive to think of a REST API **without any UI**—and you probably do as well. |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +### 2. No Authentication |
| 59 | + |
| 60 | +Now, this is where even experienced developers might get confused. |
| 61 | + |
| 62 | +Let’s start with a very basic REST API call: |
| 63 | + |
| 64 | +```bash |
| 65 | +curl http://localhost:8000/users/me |
| 66 | +``` |
| 67 | + |
| 68 | +This request is meant to return information about the **current user**—that is, the user making the HTTP request. |
| 69 | + |
| 70 | +But wait—didn’t I just say there is **no authentication**? |
| 71 | + |
| 72 | +So… who is the current user? |
| 73 | +And is there **any user** at all in the database's `users` table? |
| 74 | + |
| 75 | +Yes, the Core REST API server really **has no authentication**. None. Zero. Nada. |
| 76 | + |
| 77 | +--- |
| 78 | + |
| 79 | +### So, who is the current user? |
| 80 | + |
| 81 | +**Answer:** Whoever we say it is. |
| 82 | + |
| 83 | +The REST API is a naive creature—it trusts the information you give it. You can tell it who the current user is by using a custom HTTP header. |
| 84 | + |
| 85 | +Example: |
| 86 | + |
| 87 | +```bash |
| 88 | +curl -H "Remote-User: admin" http://localhost:8000/users/me |
| 89 | +``` |
| 90 | + |
| 91 | +This request informs REST API server to use user with username `admin` as current one. |
| 92 | +Assuming you have a user named `admin` in your database, the server will respond with details about `admin`. |
| 93 | + |
| 94 | +In fact, as long as the username exists in your database, you can perform **any REST API call** just by supplying the `Remote-User` header. |
| 95 | + |
| 96 | +> **Note:** |
| 97 | +> The REST API server has no concept of authentication. |
| 98 | +> It simply receives information about the current user via HTTP headers and trusts it. |
| 99 | +
|
| 100 | +--- |
| 101 | + |
| 102 | +### What About JWT? |
| 103 | + |
| 104 | +The `Remote-User` example works—but it’s pretty basic. |
| 105 | + |
| 106 | +A more standardized and feature-rich method is to use a **JWT (JSON Web Token)**. JWTs allow you to pass more structured information in the header—like username, user ID, roles, and more. |
| 107 | + |
| 108 | +So instead of: |
| 109 | + |
| 110 | +```http |
| 111 | +Remote-User: admin |
| 112 | +``` |
| 113 | + |
| 114 | +You might pass something like: |
| 115 | + |
| 116 | +```http |
| 117 | +Authorization: Bearer <your_jwt_token_here> |
| 118 | +``` |
| 119 | + |
| 120 | +The principle remains the same: |
| 121 | +Whatever information about the current user is provided in the HTTP headers, the REST API server will **extract it and trust it**. |
| 122 | + |
| 123 | +No validation. |
| 124 | +No verification. |
| 125 | +No actual authentication. |
| 126 | + |
| 127 | +All authentication logic is expected to happen **upstream**—in whatever system is calling the REST API (e.g. an API gateway or a separate auth service). |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +## The Core Part 2 – UI |
| 132 | + |
| 133 | +This part is basically the same as Part 1—except that instead of interacting directly with the REST API, the **end user** interacts with a **fancy UI** (the frontend). |
| 134 | +Put another way: the frontend communicates with the REST API server **on behalf of the user**. |
| 135 | + |
| 136 | + |
| 137 | + |
| 138 | +The illustration above shows the frontend (FE) running on port **5173**. |
| 139 | +That’s true **only in development mode**, where a developer can start the frontend server using: |
| 140 | + |
| 141 | +```bash |
| 142 | +yarn install |
| 143 | +yarn workspace ui dev |
| 144 | +``` |
| 145 | + |
| 146 | +Really, this setup is **identical to Part 1**, just **wrapped in a nice UI**. |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +### A Few Important Points |
| 151 | + |
| 152 | +1. **Both the frontend and backend live in the same repository**: |
| 153 | + |
| 154 | + * Frontend (FE) = TypeScript / JavaScript / CSS / HTML |
| 155 | + * Backend (BE) = REST API server in Python |
| 156 | + * GitHub repo: [papermerge/papermerge-core](https://github.com/papermerge/papermerge-core/) |
| 157 | + |
| 158 | +2. **There is still no authentication**: |
| 159 | + |
| 160 | + * The REST API server accepts whatever the upstream passes as the current user via an HTTP header— |
| 161 | + e.g., `Remote-User` or `Authorization: Bearer <JWT token>`. |
| 162 | + |
| 163 | +3. **Everything shown so far lives in one single repo**: |
| 164 | + [https://github.com/papermerge/papermerge-core/](https://github.com/papermerge/papermerge-core/) |
| 165 | + |
| 166 | + |
| 167 | +## Authentication Server |
| 168 | + |
| 169 | +Now let’s introduce one more piece of the puzzle: the **Authentication Server**. |
| 170 | + |
| 171 | +At some point, there must be a component that takes a username and password and responds with one of two answers: |
| 172 | + |
| 173 | +1. ✅ Yes – the credentials are valid, the user is authenticated. |
| 174 | +2. ❌ No – the credentials are invalid, access denied. |
| 175 | + |
| 176 | +That component is the **authentication server**. |
| 177 | + |
| 178 | +A common source of confusion is that many web frameworks (looking at you, Django!) bundle authentication logic into the framework itself. This leads to a general assumption that authentication is just part of the app—not a standalone service. |
| 179 | + |
| 180 | +In the **{{ extra.project }}** universe, the **Authentication Server is a separate web application.** |
| 181 | + |
| 182 | +!!! Remember |
| 183 | + |
| 184 | + ❗️ **Authentication Server is just another web application** ❗️ |
| 185 | + |
| 186 | +Typically, the Authentication Server displays a login form where users can enter their credentials. If the combination of username and password is valid, the server responds with a **JWT (JSON Web Token)**. |
| 187 | + |
| 188 | +This token is **cryptographically signed** using a secret. That signature allows other services to later verify the token’s origin and integrity. |
| 189 | + |
| 190 | +!!! Remember |
| 191 | + |
| 192 | + ❗️ **Authentication Server issues JWT tokens** ❗️ |
| 193 | + |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +All incoming HTTP requests are then checked for a valid JWT token. A token is considered valid if: |
| 198 | + |
| 199 | +* It is properly formed. |
| 200 | +* It was signed using the expected secret. |
| 201 | + |
| 202 | +If a request lacks a valid JWT, it is **redirected to the login form**. |
| 203 | +If it includes a valid JWT, the request proceeds to the REST API (which sits behind the UI). |
| 204 | + |
| 205 | +Here’s how this flow is illustrated: |
| 206 | + |
| 207 | + |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +### Included Authentication Server |
| 212 | + |
| 213 | +**{{ extra.project }}** includes a very basic authentication server. Its source code is here: |
| 214 | +[https://github.com/papermerge/auth-server](https://github.com/papermerge/auth-server) |
| 215 | + |
| 216 | +All components inside the gray box outlined with a brown dotted line are bundled in the official Papermerge container: |
| 217 | + |
| 218 | +```bash |
| 219 | +docker run -p 12000:80 \ |
| 220 | + -e PAPERMERGE__SECURITY__SECRET_KEY=abc \ |
| 221 | + -e PAPERMERGE__AUTH__PASSWORD=pass123 \ |
| 222 | + papermerge/papermerge:3.5.2 |
| 223 | +``` |
| 224 | + |
| 225 | +!!! Remember |
| 226 | + |
| 227 | + ❗️ Core + Auth Server = App Container ❗️ |
| 228 | + |
| 229 | + where |
| 230 | + |
| 231 | + Core = BE + FE |
| 232 | + |
| 233 | + where |
| 234 | + |
| 235 | + * BE = REST API Server |
| 236 | + * FE = Frontend Application |
| 237 | + |
| 238 | +--- |
| 239 | + |
| 240 | +### Pluggable Authentication |
| 241 | + |
| 242 | +The beauty of this design is its flexibility: the included authentication server is **basic by design**—you can easily replace it. |
| 243 | + |
| 244 | +For example, the included server does **not** support: |
| 245 | + |
| 246 | +* 2FA (Two-Factor Authentication) |
| 247 | +* User registration flows |
| 248 | + |
| 249 | +But that’s by design. You can replace it with more full-featured authentication systems like: |
| 250 | + |
| 251 | +* [Keycloak](https://www.keycloak.org/) |
| 252 | +* [Authelia](https://www.authelia.com/) |
| 253 | + |
| 254 | +--- |
| 255 | + |
| 256 | +## Workers and Redis |
| 257 | + |
| 258 | +So far, we’ve only discussed components that deal with HTTP—the **web-facing parts** of the system. |
| 259 | + |
| 260 | +Now let’s explore the **workers**. |
| 261 | + |
| 262 | +Workers are small background applications that handle long-running or asynchronous tasks. They **don’t use HTTP** to communicate. Instead, they interact with the main app via a **message queue**. |
| 263 | + |
| 264 | +* The **main app** acts as the **producer**, placing tasks onto the queue. |
| 265 | +* The **workers** act as **consumers**, picking up and executing those tasks. |
| 266 | + |
| 267 | +The transport mechanism is **Redis**, which functions as the message bus. Communication happens via **named queues**. |
| 268 | + |
| 269 | +{{ extra.project }} uses the following workers: |
| 270 | + |
| 271 | +* [Path Template Worker](https://github.com/papermerge/path-tmpl-worker) |
| 272 | +* [S3 Worker](https://github.com/papermerge/s3-worker) |
| 273 | +* [OCR Worker](https://github.com/papermerge/ocr-worker) |
| 274 | +* [i3 Worker](https://github.com/papermerge/i3-worker) |
| 275 | + |
| 276 | +--- |
| 277 | + |
| 278 | +## Path Template Worker |
| 279 | + |
| 280 | +Each document **category** in {{ extra.project }} has an associated [Jinja](https://jinja.palletsprojects.com/) path template |
| 281 | +that defines where documents in that category should be stored. |
| 282 | + |
| 283 | +Example 1 – basic template: |
| 284 | + |
| 285 | +{% raw %} |
| 286 | + |
| 287 | +```jinja |
| 288 | +{% if document.id %} |
| 289 | + /home/My Documents/Invoices/{{ document.id }}.pdf |
| 290 | +{% else %} |
| 291 | + /home/My Documents/Invoices/ |
| 292 | +{% endif %} |
| 293 | +``` |
| 294 | + |
| 295 | +{% endraw %} |
| 296 | + |
| 297 | +Example 2 – more sophisticated template: |
| 298 | + |
| 299 | +{% raw %} |
| 300 | + |
| 301 | +```jinja |
| 302 | +{% if document.has_all_cf %} |
| 303 | + /home/Receipts/{{ document.cf['Shop'] }}-{{ document.cf['Effective Date'] }}.pdf |
| 304 | +{% else %} |
| 305 | + /home/Receipts/{{ document.id }}.pdf |
| 306 | +{% endif %} |
| 307 | +``` |
| 308 | + |
| 309 | +{% endraw %} |
| 310 | + |
| 311 | +Now imagine you have **63,000 documents** in the "receipts" category, and you change the template from example 1 to example 2. The system must now **re-evaluate the path** for each of those 63,000 documents. |
| 312 | + |
| 313 | +This is a huge task—and that’s exactly what the **Path Template Worker** is for. |
| 314 | + |
| 315 | + |
| 316 | + |
| 317 | +🔗 [Path Template Worker – Source Code](https://github.com/papermerge/path-tmpl-worker) |
| 318 | + |
| 319 | +--- |
| 320 | + |
| 321 | +## S3 Worker |
| 322 | + |
| 323 | +**{{ extra.project }}** supports S3-compatible storage systems. When S3 is enabled, all documents are uploaded to the S3 bucket. |
| 324 | + |
| 325 | +The **S3 Worker** handles this upload process. |
| 326 | + |
| 327 | + |
| 328 | + |
| 329 | +The **S3 Worker** must have access to the **same local storage** used by the app (i.e., where documents are uploaded by the user). |
| 330 | + |
| 331 | +In deployments: |
| 332 | + |
| 333 | +* With **Docker Compose**: local storage is typically a **Docker volume**. |
| 334 | +* With **Kubernetes**: local storage is usually a **pod volume**. |
| 335 | + |
| 336 | +🔗 [S3 Worker – Source Code](https://github.com/papermerge/s3-worker) |
| 337 | + |
| 338 | +--- |
| 339 | + |
| 340 | +## OCR Worker |
| 341 | + |
| 342 | +📌 *Coming soon* |
| 343 | + |
| 344 | +--- |
| 345 | + |
| 346 | +## i3 Worker |
| 347 | + |
| 348 | +📌 *Coming soon* |
0 commit comments