0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

開封日まで本文を返さない:時間ロックメッセージのサーバ側設計

0
Last updated at Posted at 2026-10-28

年末になると「友達が飾ったメッセージを、特定の日まで開かない」系のプロダクトが増えます。例として Deco My Tree を取り上げます。オンラインのクリスマスツリーを作り、リンクを共有すると、友だちがオーナメントとメッセージを飾れます。ただし 12月25日までは本文が封印 され、ツリーの持ち主は「誰が飾ったか」は分かっても、中身は読めません。

deco-hero.jpg

見た目はメッセージボードに近いですが、実装でいちばん壊れやすいのはここです。封印をフロントの表示制御だけでやると、封印になっていません。 本稿では公開ページから読み取れる挙動をもとに、再利用しやすい設計の骨組みを整理します。コードはすべてサンプルコードであり、プロダクトのソースではありません。

まず制約を書き出す

日本語版の説明から、開発者向けの制約に落とし込むとだいたい次のようになります。

  • アプリ不要。ブラウザのリンクだけで使える。メッセージ送信時だけ一度ログインが必要
  • 12月25日までメッセージはロック。送信者は分かる(匿名を除く)が、本文は見えない
  • デフォルトはツリー持ち主のみ閲覧可。書き手が「みんなに公開」を選んだ場合のみ、開封後に他者も読める
  • 匿名にすると、持ち主にも「匿名」と表示される
  • オーナメントは1ページ12個。満杯になると次のページが増える。受付は通常1月1日まで

deco-privacy.jpg

「画面上に鍵アイコンを出す」ことではなく、開封日より前は、クライアントが取得できる応答に本文を載せないことが本丸です。

封印はシリアライズの段階で行う

よくある失敗は、APIが body をそのまま返し、フロントが Date.now() と開封日を比較して描画を止めるやり方です。DevTools やパケットキャプチャで全文が見えるので、封印は一瞬で崩れます。

より安全なのは、サーバがシリアライズ時に「いまの時刻 + 閲覧権限」でフィールドの有無を決めることです(サンプルコード)。

type Wish = {
  id: string;
  treeId: string;
  ornament: string;
  authorId: string;
  authorName: string;
  isAnonymous: boolean;
  isPublic: boolean;
  body: string;
  createdAt: Date;
};

// サンプル: UTC の 12-25 00:00 を開封時刻にする
const openAt = (year: number) => new Date(Date.UTC(year, 11, 25));

function serializeWish(
  wish: Wish,
  viewerId: string | null,
  ownerId: string,
  now = new Date(),
) {
  const opened = now >= openAt(now.getUTCFullYear());
  const isOwner = viewerId === ownerId;
  const canRead = opened && (isOwner || wish.isPublic);

  return {
    id: wish.id,
    ornament: wish.ornament,
    author: wish.isAnonymous ? 'Anonymous' : wish.authorName,
    sealed: !canRead,
    body: canRead ? wish.body : null,
  };
}

ポイントは3つです。

  1. 本文を欠落させる。 sealed: true のとき body は null。フロントは「未開封」状態を描くだけ。
  2. 匿名と封印を同じ出口で扱う。 DBには通報・監査用に authorId を残してよく、対外レスポンスには実名を出さない。
  3. 開封瞬間のタイムゾーン戦略を先に決める。 上記の UTC は例です。「ユーザ現地の12月25日0時」を約束するなら、ツリー作成時に timezone を保存し、比較はそのゾーンで行う方が筋が良いです。

タイムゾーン:「日付」は瞬間である

「12月25日に開封」は日付に聞こえますが、システム上はタイムゾーン付きの瞬間です。

戦略 利点 代償
固定 UTC 実装が単純で世界一斉 体感が前後する地域がある
固定の運用タイムゾーン 運用メッセージが揃う 越境ユーザへの説明が必要
ツリーごとにタイムゾーン 持ち主の現地感覚に近い シリアライズとキャッシュが複雑

どれを選んでも、開封判定の権威はサーバ時計だけです。クライアント時刻は改ざんできるので認可に使えません。UIのカウントダウン表示に留めるのが安全です。

キャッシュと CDN が開封日を「引きずる」

封印解除とは、同じ URL の応答が、ある瞬間以降に中身を変えるという意味です。ツリーページやメッセージ一覧を CDN で長くキャッシュすると、「カレンダーは25日なのに画面はまだ封印」が起きます。

取りうる制約の例です。

  • メッセージ一覧は Cache-Control: private, no-store
  • キャッシュが必要なら、TTL を開封時刻までに切り詰める(サンプル)
function cacheSeconds(now = new Date(), base = 300) {
  const untilOpen = Math.floor(
    (openAt(now.getUTCFullYear()).getTime() - now.getTime()) / 1000,
  );
  return untilOpen > 0 ? Math.min(base, untilOpen) : base;
}

開封後の短TTLは許容できますが、権限付き応答を別ユーザに流さないよう、認証付きAPIは private が無難です。

ページング:満杯になったら次のページ

deco-ornaments.jpg

公開説明ではオーナメントは12個ごとにページが増えます。ページング自体は簡単ですが、並び順が「昔の友だちのオーナメントを探しづらくする」かどうかを決めます。

const PER_PAGE = 12;

function treePage(wishes: Wish[], page: number) {
  const sorted = [...wishes].sort((a, b) => +a.createdAt - +b.createdAt);
  return {
    page,
    totalPages: Math.max(1, Math.ceil(sorted.length / PER_PAGE)),
    slots: sorted.slice((page - 1) * PER_PAGE, page * PER_PAGE),
  };
}

作成時刻の昇順で切ると、新しい祝福は末尾に付き、既存スロットが押し出されません。「ツリーを飾る」心象には、常に最新を先頭へ寄せるより向いています。

受付停止と通報

時間ロックとセットになりやすい運用機能は2つです。

  • 書き込み停止: 1月1日以降は作成を拒否。持ち主が早めに受付OFFした場合も同じ検証パスに載せる(フロントでボタンを灰にしただけ、では不十分)
  • 通報: 対外表示が匿名でも、サーバ側には追跡可能な識別子を残す。メッセージから通報でき、通報内容は管理者だけが見る——これは「シリアライズで名前を隠す」ことと矛盾しません

フロントの役割

封印がサーバに落ちたあとも、フロントの仕事ははっきりしています。

  • やる: sealed に応じたロック表示/カウントダウン(表示専用)。送信時にオーナメント種別と公開/匿名を付ける
  • やらない: 未開封の body を先読みして「時刻が来たら出す」。本文入りレスポンスを localStorage に溜める。クライアント時刻で「開封済みAPI」を呼ぶか決める

自分の書いたメッセージのプレビューも、「作者本人は読める」というサーバ規則に載せる方が、別経路のプレビューAPIより漏れにくいです。

ほかの場面への転用

「期限まで読めない」はクリスマス専用ではありません。誕生日の寄せ書き、卒業メッセージ、チームの年次レビュー、コンプライアンス上のクーリングオフ後公開など、同じ型が使えます。

  1. サーバのシリアライズで可視フィールドを決める
  2. 開封瞬間のタイムゾーン意味を明文化する
  3. キャッシュTTLを開封点より長くしない
  4. 匿名表示と監査用フィールドを分離する

実際の流れを手で確かめたい場合は、ブラウザで Deco My Tree を開き、無料でツリーを1本作ってみるとよいです。時間ロックは スタイル問題ではなく認可問題 として扱うと、後から戻るコストがかなり減ります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?