Bolik Timeline is an application for managing personal documents like notes, photos and memories. It supports offline editing, is end-to-end encrypted and is open source unlike other popular solutions.
Read an introductory blog post for more details.
flowchart LR
App["#9742; Mobile app"] --> Fly{Fly.io Anycast routing}
Fly --> S1([LiteFS])
Fly --> S2([LiteFS])
%% Warsaw section
subgraph Warsaw
S1 --> Backend1[Bolik backend]
Backend1 --> DB1
S1 --> DB1[(SQLite)]
end
%% Amsterdam section
subgraph Amsterdam
S2 --> DB2[(SQLite)]
S2 --> Backend2[Bolik backend]
Backend2 --> DB2
end
%% Consul
S1 --> Consul[(Consul)]
S2 --> Consul
%% Backblaze
Backend1 --> S3(["#128452; Backblaze B2"])
Backend2 --> S3
Mobile app fetches files directly from B2 (same as S3). It is a bit messy to add this connection to the diagram.
What happens here?
A request from Bolik Timeline application goes through fly.io's network. fly.io uses anycast routing to direct the request to the "closest" backend server (either Amsterdam or Warsaw). Backend's entry point is a LiteFS process. LiteFS replicates SQLite database across the servers and also proxies HTTP requests to Bolik backend.
Why does LiteFS work for Bolik Timeline?
In case leader dies we can tolerate some data loss because system is eventually-consistent and app will reupload lost data on the next connection.
Use cases
Let's go through a few use cases to see what actually happens.
Card upload
Card is basic editable unit in the app, think a note or a document. A card has textual content but can also contain file attachments.
All file attachments are encrypted with ChaCha20Poly1305 before upload.
sequenceDiagram
actor App as Application
participant Backend as Bolik backend
participant S3 as Backblaze B2
%% Backend
App ->> Backend: I want to upload a file of this size
activate Backend
Backend -->> App: Use this presigned URL to upload the file
deactivate Backend
%% S3
App ->> S3: Upload a file
activate S3
Note over S3: All good: URL is valid and file size matches.
S3 -->> App: Ack.
deactivate S3
%% App
App ->> Backend: Upload a card
activate Backend
Backend ->> S3: Does this file exist?
activate S3
S3 -->> Backend: Yes
deactivate S3
Note over Backend: Verify that all file attachments were uploaded
then save the card to SQLite.
Backend -->> App: Ack.
deactivate Backend
Read request
SQLite database is replicated on both servers. Read requests could be served by any node.
Write request
With writes things get a bit more complicated. When server receives a request that is about to write to a database then the server should check if this node is a leader, if it is not then this request should be forwarded to a leader node.
Fly.io supports retargeting a request out of the box. You only need to respond with a fly-replay header and Fly's load balancer will forward the request to the specified target transparently to the user.
sequenceDiagram
actor App as Application
participant Fly as Fly Load Balancer
participant Backend1 as Bolik backend in Amsterdam
participant Backend2 as Bolik backend in Warsaw
App ->> Fly: Save this card
activate Fly
Fly ->> Backend1: Save this card
activate Backend1
Note over Backend1: I am a reader and cannot handle this write request.
Backend1 -->> Fly: Forward this request to Warsaw
deactivate Backend1
Fly ->> Backend2: Save this card
activate Backend2
Note over Backend2: I am a leader and can handle this write request.
Backend2 -->> App: Ack.
deactivate Backend2
deactivate Fly
Luckily, this use case becomes simpler with the introduction of HTTP proxying in LiteFS. Your application doesn't need to worry about request forwarding. LiteFS will handle that for you.
Future work
At the moment all servers are located in the EU. While the app works in asynchronous fashion (all updates are fetched on the background) file attachments are downloaded synchronously.
First easy task would be to set up Backblaze mirroring (cloud replication) across regions. I will need to create a new account in the US and configure Backblaze to automatically mirror two buckets (EU ⇿ US). Then US users should notice a noticable speed up when downloading the files.