PubliraGitHub

An open-source platform for distributing manga under your own brand.

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.

Next.js apps
3
Go processes
2
Mobile client
Flutter
Reader languages
5
License
Apache-2.0

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.

publisher.example / catalog
The Publira reader-facing catalog, led by the work the tenant is featuring
The catalog of a tenant site, running on the development seed data.

What a publisher gets on day one

A publisher gets a reader-facing site, an app, an editorial console, and — for whoever runs the install — a console above them all.

  • Multi-tenant from the ground up

    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.

  • One console for the whole catalog

    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.

  • Themes with a real preview

    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.

  • Paid episodes, through the provider you choose

    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.

  • Free to read, on the tenant's schedule

    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.

  • Sign in with a passkey, Apple, or Google

    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.

  • The site, the app, or both

    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.

  • Royalties, closed month by month

    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.

  • Age ratings under the tenant's own rule

    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.

Four surfaces, one catalog

Three Next.js apps, a Flutter client, and the mail they send, each with its own audience.

The reader-facing site

apps/web-storefront

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.

GenresEach genre drawn from the covers of the series it ranks highest, or from artwork the editors upload for it, with the number of series behind it. A genre opens into its most-read series of the week above all of its series, sorted and narrowed the way the whole list is, and the genres themselves are the tenant's own classification rather than a fixed list.
Series detailThe cover, synopsis, label, genres, and tags, with every author in the role they are credited in, and Follow and Share beside them. The episodes run below in reading order, each marked free or paid, so what can be read now is clear before anyone signs in.
Episode viewerPages drawn on a canvas and turned as spreads, right to left, from the page the editor set the spreads to start on. The toolbar's slider moves a spread at a time, and past the last page the reader reacts to the episode and is offered the next one. The viewer is the package published on npm, so the paging, the spread, and the direction are the library's behaviour.

The tenant console

apps/web-admin

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.

DashboardPublished series, draft episodes, and scheduled releases, above the queue of everything still needing attention. The queue gathers the drafts and the upcoming releases with when each goes out, so an editor opening the console is shown what is waiting rather than having to go looking for it.
EpisodesEvery episode of a series in reading order, dragged into place — or moved with an arrow key — rather than renumbered by hand. An episode is scheduled for a time in the tenant's own zone, and its pages go up as images, a ZIP, or an EPUB, are reordered the same way, and can each be replaced or deleted even after the episode is out.
ThemeThe brand colors, the text drawn on each of them, and the typefaces readers see first, beside the logo and the icon the public site carries, with a preview built from the parts the public site is made of. The preview follows every edit while the public site keeps its current colors, so nothing reaches readers until the theme is saved.

The platform console

apps/web-platform

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.

DashboardWhat the install still needs set up, with a way into each setting, above the count of tenants and the recent cross-tenant events — tenants created, roles granted, accounts added. An operator opening the console sees whether the install is ready before anything else.
TenantsEvery registered tenant with its status and the day it was created, searchable by name or ID, and each one's profile, domains, and members. This console manages a tenant's status and who administers it, and stays out of the catalog, which each tenant runs from its own console.
Install settingsThe one S3-compatible bucket every tenant's uploads go to, the SMTP relay the install sends through, the limits that guard sign-in and mail, and the key pair Web Push signs with. A saved change reaches every running server without a restart.

The mobile app

mobile

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.

CatalogThe tenant's name and theme in the app bar, the announcement the console pinned, and the episode the reader stopped at, above the genres and the week's chart. A bar along the bottom reaches home, search, the library, notifications, and the account from every screen but the viewer.
Series and authorsCover, credits in the roles the tenant defined, label, genres, and tags, with a follow for the series and for each author and Share handing out the site's own link. An author's page carries the same follow, and a new episode from someone the reader follows arrives as a push notification that opens straight into the viewer.
Episode viewerThe episode the site serves, laid out from the spread and the reading direction the editor set on it, with a slider that moves through the pages. A paid episode is bought in the store's own sheet or through the site's checkout, whichever the tenant chose, and a screenshot of a page is met with the tenant's notice on how its pages may be used.

The mail it sends

apps/email-renderer

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.

Confirming an addressThe first mail a new reader receives: the button that confirms the address, when the link expires, and the link written out for a client that shows no buttons.
Resetting a passwordThe link that sets a new password, expiring a day after it was sent, in the same layout the tenant's other mail uses.
A message for staffWhat a reader sent through the contact form, with the address to answer at and the account they were signed in as, so staff can answer from the message's page in the console or from their own mail client.

How the pieces fit together

Next.js at the front, one Go server behind Connect RPC with a worker beside it, and nothing underneath that you cannot already run.

  1. ClientsBrowserFlutter app
  2. EdgeA reverse proxy
  3. Web (Next.js)web-storefrontweb-adminweb-platform
  4. TransportConnect RPC over Protocol Buffers
  5. Servers (Go)publira serverpublira worker
  6. InfrastructurePostgreSQLRedis-compatible cacheS3-compatible storageSMTP

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.

What it needs to runOrdinary infrastructure with no managed service behind it — bring your own, or keep the ones the Compose file brings up for you.
DependencyWhat it holds
A reverse proxyRequiredRoutes 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.
PostgreSQLRequiredThe catalog, tenants, users, purchases, and the audit log. Each server connects as its own role.
A Redis-compatible cacheRequiredThe shared Next.js cache, so a deployment can run more than one instance behind a load balancer.
S3-compatible object storageRequiredEpisode 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 relayRequiredAccount mail, notifications, and the notices staff receive, configured per platform and per tenant.
OpenSearch or ElasticsearchOptionalA 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 providerOptionalOne 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 providerOptionalSendGrid, 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 projectOptionalOne per tenant app. Push notifications to the mobile app, sent through Firebase Cloud Messaging, which relays to APNs for iOS.
An OpenTelemetry collectorOptionalTraces from the Next.js apps and the Go servers, and metrics from the Go servers; off unless enabled.

Running an install

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.

Download the release
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 generate

The 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.