The reader-facing site
apps/web-storefrontThe site a reader lands on, published under the publisher's own brand. Browsing, searching, ranking, following, signing in, and the viewer that turns the pages.
Publira is a multi-tenant SaaS that gives publishers with limited IT resources a digital distribution platform for manga, run under their own brand. Editors submit the book information they receive from creators, and readers read it on the web or on mobile.
Still being built
Publira is under active development, and its newest release, v0.3.2, is not ready for production use yet. Everything shown on this page runs today, and an install can be brought up from the images each release publishes with one Compose file, but running it for a real publisher still means reading the code, adapting it to your own environment, and taking the changes each 0.x minor release may bring. Take it as something to evaluate, build on, and contribute to — not as a finished product.

Publira is the platform. Some of the pieces it needed grew into libraries of their own, each published for anyone to use without the rest of it.
A publisher gets a reader-facing site, an app, an editorial console, and — for whoever runs the install — a console above them all.
Every publisher runs under its own brand and domain. Above them, the platform console — or publiractl, for an install that runs without it — manages the tenants, their readers, and the settings the whole install shares.
The catalog, the pages, and the announcements are edited in one console, with a queue of everything awaiting release. Readers' contact messages are answered there too, and each member of the staff sees only the controls their role may use.
Colors, typefaces, logo, and icon are edited against a live preview built from the same parts as the public site, and the site keeps its current theme until this one is saved.
A tenant sells episodes through Stripe or PAY.JP on the web and through the App Store and Google Play in its app. A purchase is written only when the provider's signed notification arrives, not on the redirect, and the reader is mailed a receipt. Staff refund a web purchase from the console, and a full refund or a dispute closes the episode again.
A paid episode opens to every reader for a free reading period, scheduled on one episode or across many episodes of a series at once. A series can also offer free if you wait: signed-in readers open its paid episodes free, one at a time, with a ticket that comes back after a wait.
Readers sign in to the site and the app with an email address, a passkey that works on both, or the Apple and Google credentials each tenant registers. Tenant staff and the platform's operators can turn on two-step verification, which staff can answer with a passkey, and the platform can require it of every tenant administrator and every operator.
A series can be kept to the site or to the app, and an episode can narrow it further; the server answers every catalog read for the surface that asks. Where an episode is sold is set the same way, from a tenant default down to the episode itself.
Each credited author carries a share of an episode's sales. A month closes into a statement — by hand, or on the day the tenant chooses — and a closed statement downloads as CSV. An author linked to a reader account reads their own episodes without buying them, and cannot rate them.
R15 and R18 titles are marked wherever they are listed, hidden until the reader confirms, gated on the birth date the account carries under the verification rule each tenant chooses, and ranked in charts of their own that only a reader who may see them can open.
Three Next.js apps, a Flutter client, and the mail they send, each with its own audience.
The site a reader lands on, published under the publisher's own brand. Browsing, searching, ranking, following, signing in, and the viewer that turns the pages.
Where editors work: the catalog and the publishing queue, the payment and sign-in providers, the theme and the pages of the public site, the messages readers send, the purchases they made and their refunds, what each author is owed, and the record of every change made to any of it.
One level above the tenants, for whoever operates the install: what the install still needs set up, tenant status, cross-tenant events, the readers of every tenant, and the settings every tenant shares, from object storage to security policy.
The same catalog on a phone, from one Flutter codebase for Android and iOS, under the tenant's own name and theme. It calls the public API directly rather than going through the site, signs a reader in with an email address, a passkey, Apple, or Google, sells episodes through the store or the site's checkout, and keeps the episodes a reader has opened or saved on the device.
Account mail and the notices staff receive, drawn as HTML from React components by a renderer of its own and sent with a plain-text part beside it. Mail to a tenant's readers and staff carries the tenant's name rather than Publira's and is written in the tenant's language; in Japanese, Korean, and Chinese the spacing between scripts is left to the mail client's text rendering, as it is on the site.
Next.js at the front, one Go server behind Connect RPC with a worker beside it, and nothing underneath that you cannot already run.
One publira server process carries the public, admin, and platform namespaces and delivers the images for both the reader’s origin and the console’s. A second one started with --role storefront can take the tenants’ domains alone, holding only the reader-facing database login. publira worker, from the same binary, delivers the mail and the push notifications and runs every scheduled job: promoting due episodes, applying the free-window boundaries, turning over each tenant’s calendar day, rebuilding the charts, closing royalty months, and purging what has outlived its retention. Running it is all the scheduling a deployment needs, and on a single host the two can run as one process, publira server --with-worker. An optional email-renderer draws the HTML part of each mail, and publiractl migrates the database, creates its roles, sets an install up, and runs any of the worker’s jobs by hand.
Every layer is instrumented with OpenTelemetry, so one browser request reads as a single trace: the Next.js root span, the Connect RPC it makes during SSR, the identically named span on the Go side, and the db.query children below it. Point it at whatever collector you already run, or leave tracing off — it is disabled by default. The two Go processes export OpenTelemetry metrics as well, which stay off until enabled in the same way.
| Dependency | What it holds |
|---|---|
| A reverse proxyRequired | Routes each tenant's hosts to its site and console, and the public API and the images to the server. The repository ships configurations for Traefik, nginx, and Caddy. |
| PostgreSQLRequired | The catalog, tenants, users, purchases, and the audit log. Each server connects as its own role. |
| A Redis-compatible cacheRequired | The shared Next.js cache, so a deployment can run more than one instance behind a load balancer. |
| S3-compatible object storageRequired | Episode pages, eye-catches, and the other uploads. The bucket is saved with the install's settings rather than in each process's environment. |
| An SMTP relayRequired | Account mail, notifications, and the notices staff receive, configured per platform and per tenant. |
| OpenSearch or ElasticsearchOptional | A catalog search ranked by relevance, for an install that wants one; every node needs the analysis-kuromoji and analysis-icu plugins. Without it, the search runs on PostgreSQL. |
| A payment providerOptional | One account per tenant, for paid episodes only: Stripe or PAY.JP on the web, and the App Store and Google Play in the app. A signed notification from the provider is what confirms a purchase. |
| An inbound email providerOptional | SendGrid, Resend, Mailgun, Amazon SES, or a mail server that posts what it receives over HTTPS, per tenant, for a tenant that wants readers' replies to its answers filed under their contact messages in the console. |
| A Firebase projectOptional | One per tenant app. Push notifications to the mobile app, sent through Firebase Cloud Messaging, which relays to APNs for iOS. |
| An OpenTelemetry collectorOptional | Traces from the Next.js apps and the Go servers, and metrics from the Go servers; off unless enabled. |
An install runs from container images and one Compose file, and publiractl brings its database and its first tenant into service. The host needs Docker and nothing else.
PUBLIRA_VERSION=0.3.2
curl -fsSLO \
https://github.com/publira/publira/releases/download/v$PUBLIRA_VERSION/publira-deploy-$PUBLIRA_VERSION.tar.gz
tar -xzf publira-deploy-$PUBLIRA_VERSION.tar.gz
cd publira-deploy-$PUBLIRA_VERSION
# Write .env from .env.example, with every secret generated
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/work -w /work \
ghcr.io/publira/publiractl:$PUBLIRA_VERSION env generateThe archive holds the release's Compose file, the proxy's Caddyfile, and an .env.example. publiractl env generate writes .env from it, with every secret generated.
Every release publishes its images to the GitHub Container Registry under its version, such as ghcr.io/publira/publiractl:0.3.2, for linux/amd64 and linux/arm64. A host pulls them without signing in, so nothing is built on it.
PUBLIRA_IMAGE_TAG in .env already names the release's version, so do not change it. The images have to come from the same release as the files beside .env: publiractl carries the migrations of its release, the processes expect the schema of theirs, and the proxy runs the routing of the Caddyfile in the archive.
An install that changes the code, or runs on another platform, builds its own images from a checkout of the repository and names them instead.