PubliraGitHub

マンガを自社ブランドで配信できる、オープンソースのプラットフォーム。

Publiraは、ITリソースの限られた出版社がマンガのデジタル配信プラットフォームを自社ブランドで運営するためのマルチテナントSaaSです。編集者は作家から預かった作品を書誌情報とともに入稿し、読者はそれをWebやモバイルアプリで読みます。

Next.jsアプリ
3
Goプロセス
2
モバイルクライアント
Flutter
読者向けの対応言語
5
ライセンス
Apache-2.0

開発中

Publiraは開発中で、最新のリリースであるv0.3.2もまだ本番環境で使える段階ではありません。このページで紹介している機能はすべて現時点で動作しており、リリースごとに公開されるイメージとひとつのComposeファイルで環境を立ち上げることもできます。ただし実際の出版社で運用するには、コードを読んで自分の環境に合わせ、0.xのマイナーリリースごとに入りうる変更にも追従する必要があります。完成した製品ではなく、動作を検証し、手を加え、開発に参加していただくためのプロジェクトとして公開しています。

publisher.example / catalog
出版社が特集する作品を先頭に表示したPubliraの読者向けカタログ
開発用のシードデータで表示したテナントのサイトのカタログです。

プラットフォームと、そこから生まれたライブラリ

Publiraの中心はプラットフォームです。開発の過程で必要になった部品のいくつかは独立したライブラリとして切り出し、プラットフォームがなくても誰でも使えるように公開しています。

出版社が最初から使える機能

出版社には読者向けのサイトとアプリ、編集用のコンソールが用意されます。環境全体を運用する人には、それらをまとめて管理するコンソールがあります。

  • 最初からマルチテナントを前提に設計

    出版社はそれぞれ自社のブランドとドメインでサイトを運営できます。その上位にあるプラットフォームコンソール (これを動かさない環境ではpubliractl) で、テナントとその読者、環境全体で共有する設定を管理します。

  • カタログの管理をひとつのコンソールで

    作品のカタログやページ、お知らせはひとつのコンソールで編集でき、公開待ちのものはキューで一覧できます。読者からのお問い合わせにもここから回答でき、各スタッフにはそのロールで使える操作だけが表示されます。

  • 実際の表示を確認しながらテーマを編集

    カラーや書体、ロゴ、アイコンは、公開サイトと同じ部品で組み立てたプレビューで実際の表示を確かめながら編集できます。保存するまで公開サイトの見た目は変わりません。

  • 選んだ決済サービスで有料エピソードを販売

    テナントはWebではStripeかPAY.JPで、アプリではApp StoreとGoogle Playでエピソードを販売します。購入が記録されるのはリダイレクトの時点ではなく、決済サービスから署名付きの通知が届いた時点で、読者には購入完了のメールが送られます。Webでの購入はスタッフがコンソールから返金でき、全額返金や異議申し立てがあると、読者はそのエピソードを再び読めなくなります。

  • テナントが決める無料公開

    有料エピソードは、設定した無料公開期間のあいだすべての読者が読めます。期間はエピソードごとにも、シリーズの複数のエピソードにまとめても設定できます。シリーズでは「待てば無料」も提供でき、サインインした読者はチケットを使って有料エピソードを1話ずつ無料で読めます。使ったチケットは一定時間待つと再び使えるようになります。

  • パスキー、Apple、Googleでサインイン

    読者はメールアドレスのほか、サイトとアプリの両方で使えるパスキーや、テナントごとに設定したAppleとGoogleのアカウントでサインインできます。テナントのスタッフとプラットフォームのオペレーターは2段階認証を有効にでき (スタッフはパスキーでも認証できます)、プラットフォームはすべてのテナント管理者とオペレーターに2段階認証を必須にできます。

  • Webとアプリで表示先を分ける

    作品はWebだけ、アプリだけ、またはその両方に表示でき、エピソード単位でさらに絞り込めます。サーバーはWebサイトとアプリそれぞれからのリクエストに、そこで表示できる作品とエピソードだけを返します。販売先も同じように、テナントの既定値からエピソード単位まで設定できます。

  • 印税は月ごとに締めて明細に

    クレジットされた著者ごとに、エピソードの売上に対する配分率を設定します。月は毎月手動で締めるか、テナントが選んだ日に自動で締めることができ、締めた月は明細としてCSVでダウンロードできます。読者アカウントと紐づけた著者は自分のエピソードを購入せずに読めますが、評価はできません。

  • テナント独自のルールで運用する年齢制限

    R15とR18の作品は、どの一覧でもマークが付き、読者が確認するまで伏せて表示されます。閲覧はテナントが選んだ確認方法に従い、アカウントの生年月日で制限されます。ランキングもレーティングごとに分かれ、閲覧できる読者だけが開けます。

読者、編集者、オペレーターのためのアプリ

Next.jsで作った3つのWebアプリとFlutterアプリ、それらが送るメールです。どれも対象とする利用者が異なります。

読者向けサイト

apps/web-storefront

読者が訪れるサイトで、出版社自身のブランドで公開されます。作品の閲覧や検索、ランキング、フォロー、サインイン、ページをめくって読むビューアーを備えています。

ジャンル各ジャンルは、そのジャンルで上位の作品の表紙か、編集者がアップロードしたアートワークで表示され、作品数が添えられます。ジャンルを開くと、今週よく読まれている作品の下にそのジャンルの全作品が並び、作品一覧と同じように並べ替えや絞り込みができます。ジャンルは固定のリストではなく、テナントが独自に設定する分類です。
シリーズ詳細表紙、あらすじ、レーベル、ジャンル、タグに加えて、原作や作画といった役割とともにすべての著者が表示され、フォローと共有のボタンがあります。その下にはエピソードが読む順に並び、無料か有料かが示されるので、サインインする前から今すぐ読めるエピソードがわかります。
エピソードビューアーページはcanvasに描画され、編集者が設定した見開きの開始ページから、右から左へ見開き単位でめくります。ツールバーのスライダーも見開き単位で移動し、最後のページを過ぎると読者はエピソードにリアクションでき、次のエピソードに進めます。ビューアーはnpmで公開しているパッケージそのものなので、ページ送りや見開き、読む方向はライブラリの機能です。

管理コンソール

apps/web-admin

編集者が作業するコンソールです。カタログと公開キュー、決済とサインインのサービス、公開サイトのテーマとページ、読者からのメッセージ、読者の購入と返金、著者への支払額を扱い、これらへの変更はすべて記録されます。

ダッシュボード公開中の作品、下書きのエピソード、公開予定の件数の下に、対応が必要なものをまとめたキューがあります。キューには下書きと公開予定のものが公開日時とともに並ぶので、編集者はコンソールを開くだけで対応が必要なものを確認できます。
エピソード作品のエピソードを読む順に並べ、番号を振り直すのではなくドラッグで (矢印キーでも) 並べ替えます。公開日時はテナントのタイムゾーンで予約でき、ページは画像、ZIP、EPUBのいずれかでアップロードして同じように並べ替えられます。公開後でもページごとに差し替えや削除ができます。
テーマブランドカラーとその上に載る文字の色、読者が最初に目にする書体を、公開サイトに表示するロゴやアイコンと並べて設定し、公開サイトと同じ部品で組み立てたプレビューで確認できます。プレビューは編集のたびに更新されますが、公開サイトの見た目はテーマを保存するまで変わりません。

プラットフォームコンソール

apps/web-platform

プラットフォーム全体を運用するオペレーター向けのコンソールです。環境にまだ必要な設定、テナントの状態、テナントをまたぐイベント、全テナントの読者を管理し、オブジェクトストレージからセキュリティポリシーまで全テナント共通の設定もここで行います。

ダッシュボード環境にまだ必要な設定と各設定画面へのリンクの下に、テナント数と、テナントの作成やロールの付与、アカウントの追加といったテナントをまたぐ最近のイベントが並びます。オペレーターはコンソールを開くとまず、環境の準備が整っているかを確認できます。
テナント登録されたすべてのテナントを状態と作成日とともに一覧にし、名前かIDで検索できます。テナントを開くとプロフィール、ドメイン、メンバーが表示されます。このコンソールが扱うのはテナントの状態と管理者だけで、カタログには関わりません。カタログは各テナントが自分のコンソールで管理します。
環境の設定全テナントのアップロード先となるS3互換のバケット (環境でひとつ)、メール送信に使うSMTPリレー、サインインとメール送信を保護するレート制限、Web Pushの署名に使う鍵ペアを設定します。保存した変更は、再起動しなくても稼働中のすべてのサーバーに反映されます。

モバイルアプリ

mobile

同じカタログをスマートフォンで読めるアプリです。AndroidとiOSをひとつのFlutterコードベースでまかない、テナントの名前とテーマで表示されます。サイトを経由せずに公開APIを直接呼び出し、メールアドレス、パスキー、Apple、Googleでサインインでき、エピソードはストアかサイトの決済で購入できます。読者が開いたエピソードや保存したエピソードは端末に保持されます。

カタログアプリバーにはテナントの名前とテーマが反映され、コンソールで固定したお知らせと読みかけのエピソードの下に、ジャンルと今週のランキングが続きます。画面下部のバーからは、ビューアー以外のどの画面からでもホーム、検索、ライブラリ、通知、アカウントに移動できます。
作品と著者表紙、テナントが定めた役割でのクレジット、レーベル、ジャンル、タグに加えて、作品と著者それぞれのフォローボタンと、サイトのリンクを共有するボタンがあります。著者のページにも同じフォローボタンがあり、フォローしている作品や著者の新しいエピソードはプッシュ通知で届いて、タップするとそのままビューアーで開きます。
エピソードビューアーサイトと同じエピソードを、編集者が設定した見開きと読む方向でレイアウトし、スライダーでページを移動できます。有料のエピソードは、テナントの設定に応じてストアの購入画面かサイトの決済で購入します。ページのスクリーンショットを撮ると、ページの利用についてのテナントの注意書きが表示されます。

送られるメール

apps/email-renderer

アカウント関連のメールとスタッフへの通知です。専用のレンダラーがReactコンポーネントからHTMLを生成し、プレーンテキストのパートと一緒に送ります。テナントの読者やスタッフに届くメールにはPubliraではなくテナントの名前が入り、テナントの言語で書かれます。日本語、韓国語、中国語で英数字と隣り合う部分のスペースは、サイトと同じくメールクライアントの描画に任せています。

アドレスの確認新しい読者が最初に受け取るメールです。アドレスを確認するボタン、リンクの有効期限、ボタンを表示できないメールクライアント向けのリンクが記載されています。
パスワードの再設定新しいパスワードを設定するためのリンクで、送信から1日で有効期限が切れます。テナントのほかのメールと同じレイアウトです。
スタッフへのメッセージ読者が問い合わせフォームから送った内容に、返信先のアドレスとサインインしていたアカウントが添えられるので、スタッフはコンソールのメッセージのページからでも、自分のメールクライアントからでも回答できます。

全体の構成

前面にNext.js、その背後にConnect RPCで通信するGoのサーバーとワーカーがあります。その下にあるのは、どれも自分で運用できる一般的なソフトウェアです。

  1. クライアントブラウザFlutterアプリ
  2. エッジリバースプロキシ
  3. Web (Next.js)web-storefrontweb-adminweb-platform
  4. 通信Connect RPC over Protocol Buffers
  5. サーバー (Go)publira serverpublira worker
  6. インフラPostgreSQLRedis互換のキャッシュS3互換のストレージSMTP

publira serverのプロセスひとつが公開API、管理API、プラットフォームAPIの名前空間を受け持ち、読者向けとコンソール向けの両方のオリジンに画像を配信します。--role storefrontで起動したもうひとつのプロセスにテナントのドメインだけを担当させ、読者向けのデータベースログインだけを持たせることもできます。同じバイナリのpublira workerはメールとプッシュ通知を配信し、公開予定のエピソードの公開、無料期間の開始と終了の反映、テナントごとの日付の切り替え、ランキングの再集計、印税の月締め、保持期間を過ぎたデータの削除といった定期ジョブをすべて実行します。デプロイに必要なスケジューリングはこれを動かすだけで済み、1台のホストならpublira server --with-workerとして両方をひとつのプロセスで動かすこともできます。任意のemail-rendererはメールのHTMLパートを生成し、publiractlはデータベースのマイグレーションとロールの作成、環境の初期設定を行うほか、ワーカーのジョブを手動で実行することもできます。

すべての層がOpenTelemetryで計装されており、ブラウザからのひとつのリクエストをひとつのトレースとして追えます。Next.jsのルートスパンから、SSR中のConnect RPC、Go側の同名のスパン、その下のdb.queryまでがつながります。すでに運用しているコレクターに送るだけで使え、使わなければ無効のままにできます (既定では無効です)。Goの2つのプロセスはOpenTelemetryのメトリクスも出力でき、こちらも同じように有効にするまではオフです。

動作に必要なものマネージドサービスを前提としない一般的なインフラです。自分で用意しても、Composeファイルが立ち上げるものをそのまま使ってもかまいません。
依存先用途
リバースプロキシ必須テナントのホストをそのサイトとコンソールに、公開APIと画像をサーバーに振り分けます。Traefik、nginx、Caddyの設定がリポジトリに同梱されています。
PostgreSQL必須カタログ、テナント、ユーザー、購入、監査ログを保持します。各サーバーはそれぞれ専用のロールで接続します。
Redis互換のキャッシュ必須Next.jsの共有キャッシュです。ロードバランサーの背後で複数のインスタンスを動かせるようにします。
S3互換のオブジェクトストレージ必須エピソードのページやアイキャッチなどのアップロード先です。バケットは各プロセスの環境変数ではなく、環境の設定として保存します。
SMTPリレー必須アカウントのメール、通知、スタッフへのお知らせを送ります。プラットフォームとテナントのそれぞれで設定します。
OpenSearchまたはElasticsearch任意関連度順のカタログ検索がほしい環境で使います。すべてのノードにanalysis-kuromojiとanalysis-icuのプラグインが必要です。なければ検索はPostgreSQLで動きます。
決済サービス任意テナントごとにひとつのアカウントで、有料エピソードにだけ必要です。WebではStripeかPAY.JP、アプリではApp StoreとGoogle Playを使います。購入を確定させるのは決済サービスからの署名付きの通知です。
メール受信サービス任意回答への読者の返信を、コンソールのお問い合わせメッセージの下に記録したいテナントだけが使います。SendGrid、Resend、Mailgun、Amazon SES、または受信したメールをHTTPSで転送できるメールサーバーをテナントごとに設定します。
Firebaseプロジェクト任意テナントのアプリごとにひとつ必要です。モバイルアプリへのプッシュ通知をFirebase Cloud Messaging経由で送り、iOS向けにはAPNsへ中継されます。
OpenTelemetryのコレクター任意Next.jsアプリとGoサーバーのトレースと、Goサーバーのメトリクスを受け取ります。有効にしない限りオフです。

環境を立ち上げる

環境はコンテナイメージとひとつのComposeファイルで動き、publiractlでデータベースと最初のテナントを使える状態にします。ホストに必要なのはDockerだけです。

リリースをダウンロードする
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

アーカイブには、そのリリースのComposeファイル、プロキシのCaddyfile、.env.exampleが入っています。publiractl env generateはこの.env.exampleから.envを書き出し、シークレットをすべて生成して埋めます。

リリースごとに、ghcr.io/publira/publiractl:0.3.2のようにそのバージョンでイメージがGitHub Container Registryに公開されます。linux/amd64とlinux/arm64に対応し、サインインせずに取得できるので、ホストでは何もビルドしません。

.envのPUBLIRA_IMAGE_TAGにはすでにそのリリースのバージョンが入っているので、書き換えないでください。イメージは.envと同じディレクトリにあるファイルと同じリリースでなければなりません。publiractlはそのリリースのマイグレーションを持ち、各プロセスはそのリリースのスキーマを前提にし、プロキシはアーカイブのCaddyfileのルーティングで動くためです。

コードを変更する環境やほかのプラットフォームで動かす環境は、リポジトリのチェックアウトから自分でイメージをビルドしてそれを指定します。