Skip to content

Commit 6671c8c

Browse files
authored
High level architecture (#119)
1 parent 1cdf5e4 commit 6671c8c

File tree

12 files changed

+1648
-86
lines changed

12 files changed

+1648
-86
lines changed

.DS_Store

6 KB
Binary file not shown.

docs/.DS_Store

8 KB
Binary file not shown.

docs/admin-manual/overview.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Overview
32

43
In this section, you will find information relevant to the administration and maintenance of the

docs/developer-manual/.DS_Store

6 KB
Binary file not shown.
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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+
![REST API](./architecture/1-core-rest-api.svg)
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+
![REST API and FE](./architecture/2-core-rest-api-and-fe.svg)
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+
![REST API, FE, and Auth Server](./architecture/3-be-fe-auth.svg)
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+
![Path Template Worker](./architecture/4-path-template-worker.svg)
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+
![S3 Worker](./architecture/5-s3-worker-simple.svg)
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

Comments
 (0)