アカウント登録もログインもいりません。アプリのインストールも不要です。メッセージのほか、画像や動画も送れます。
鍵付き秘密基地チャットを使う前に、以下の利用規約を確認してください。下のチェックを入れないと先に進めません。同意はこの端末に保存され、次回からは表示されません(規約はいつでも画面下の「利用規約」から見直せます)。
アカウント登録は不要です。同意のあとは、ニックネームだけで「登録なしで試す」からそのまま部屋を作れます。運営者セットアップはお試しの前提ではありません。
アカウント登録もログインもいりません。アプリのインストールも不要です。メッセージのほか、画像や動画も送れます。
ボタンを押すだけで、そのまま部屋の作成・入室・メッセージ投稿まで試せます。
データはこの端末の中だけに保存されます。
(未設定)
部屋名と合言葉を決めると、6文字の部屋コードが発行されます。
合言葉は6文字以上で設定してください。
部屋コードと合言葉を入力してください。一度入った部屋は、次回から合言葉なしで入れます。
ハイフンなし・小文字でもOKです。
友だちに伝える時はこのコードと合言葉を一緒に伝えてください。
運営者向けの画面はここにまとめています。開きたいものを選んでください。
参加者からの通報と、端末上の自動判定で「性的コンテンツの可能性が高い」とされた投稿のアラート一覧です。アラートに記録されるのはメタ情報だけ(部屋名・投稿者名・時刻・理由・判定スコア)で、画像・動画そのものがここへ自動転送されることはありません。対象の投稿は通常どおり表示されているため、問題がある場合はここから後追いで削除してください。
最近の投稿の一覧です。投稿者本人が削除した投稿も、保持期間内はここでは通常の投稿と同じ見た目でそのまま残り、内容を確認できます(投稿者名の横に小さく「(投稿者削除)」とだけ表示されます/メンバー側の部屋には表示されません)。画像・動画のファイルも保持期間内は残り、保持期間を過ぎると従来どおり自動削除されます。
無料枠の保存容量(1GB)を超えないための管理です。保持期間を過ぎた画像・動画は、アプリ起動時と部屋を開いた時に自動で削除されます。今すぐまとめて消すこともできます。テキストは容量をほぼ食わないため、初期設定では削除対象外です。
現在の保持期間:30日
削除すると対象の画像・動画ファイルは元に戻せません。チャット上では「期限切れで削除されました」の表示に置き換わります。
部屋を選ぶと参加メンバーが表示され、メンバーの退出処理(参加記録の削除)ができます。部屋の閉鎖は、その部屋・参加記録・メッセージをまとめて削除します(元に戻せません)。
この設定は、運営者だけが行う任意の後付け設定です。一般の利用やお試しの前提条件ではありませんし、アカウント登録でもありません。登録なしで試したい方は、ホームの大きなボタン「登録なしで試す」からそのまま部屋を作れます。
本番として知り合い同士で端末をまたいで使う場合にだけ、運営者(持ち主)が自分の Supabase プロジェクトの接続情報をここに入力します。手順は下の「セットアップ手順を見る」から開けます。試すだけなら、この画面は開かなくて大丈夫です。
本番利用の手順(Supabase の作成・SQL の実行・接続情報の入力・配布)はこちらから。運営者確認のあとで開きます。
anon key について:ここに入力する anon key は、ブラウザに渡す前提の「公開してよいキー」です(秘密キーではありません)。データの読み書き範囲は Supabase 側の RLS ポリシーで決まります。この配布版の SQL では、アプリの動作に必要な操作だけを許可する RLS ポリシーを設定します。
service_role キー(管理者キー)について:service_role キーは絶対に公開・埋め込みしないでください。このページに入力するのも、招待リンクや配布ファイルに含めるのも anon key だけです。service_role キーは Supabase のダッシュボード内だけで使う管理者専用のキーです。
データの保存先:部屋・メッセージ・画像・動画は、すべて運営者の Supabase プロジェクトに保存されます。
接続設定を埋め込んだ招待リンクです。友だちがこのリンクを開くと、設定済みの状態でそのまま使えます(設定は URL の # 以降=フラグメントに入るため、サーバーには送信されません)。リンクを渡した相手はあなたと同じプロジェクトに接続するので、渡す相手は知り合いまでにしてください。
このページを置いたホスティング先(Cloudflare Pages など)のURLを貼ってください。招待リンクはこのURLを土台に生成されます。
無料枠の保存容量は 1GB です。画像・動画が増え続けると上限に当たるため、古いメディアを自動で消して空ける仕組みが入っています。期限を過ぎたメディアはアプリ起動時と部屋を開いたタイミングで自動削除され、チャット上では「期限切れで削除されました」と表示されます。
この設定はこの端末に保存され、この端末から開いた時の自動削除に使われます。Supabase 接続時は実ファイル(Storage の media)と投稿のメディア参照を消し、ローカルプレビュー時は端末内のデータを消します。
部屋内の画像・動画に、カジュアルな保存を防ぐセットが入っています。① 長押し保存メニュー(コンテキストメニュー)とドラッグ保存を無効化、② 動画はダウンロードボタンを非表示(controlsList="nodownload" 等)、③ Supabase接続時はファイルを直接指す恒常的なURLを画面に渡さず、表示のたびに約10分だけ有効な署名付きURLを発行します(お試しモードは端末内表示のみ)。
※ これらは「簡単に保存できなくする」ための仕組みです。あわせて利用規約第11条で「他の利用者の画像・動画を、本人の許可なく保存・外部へ共有しないこと」をルールとして明記しています。
ホームの「登録なしで試す」と同じものです。Supabase を使わず、この端末の中だけで部屋と履歴を保存します。他の人の端末とはつながりませんが、部屋作成・入室・投稿の操作は全部試せます。本番で配布して使う場合にだけ、上の接続設定(運営者の任意設定)が必要です。
お試しだけなら、この手順は不要です。ホームの「登録なしで試す」を押せば、登録もセットアップもなしで部屋作成・投稿まで試せます。以下の手順は、知り合い同士で端末をまたいで本番利用する場合に、運営者だけが行う任意の後付け設定です。
画面はこのページ1枚(静的ホスティングに無料で置けます)、データの置き場は Supabase の無料プランです。お金がかかるのは、将来 Google Play に出す場合の開発者登録料($25・1回だけ)だけです。
media バケット、必要最小限の操作だけを許可する RLS ポリシーがまとめて作られます。-- 鍵付き秘密基地チャット セットアップSQL(Supabase SQL Editor にそのまま貼る)
-- rooms / members / messages テーブル、media バケット、必要最小限のRLSポリシー
create extension if not exists pgcrypto;
create table if not exists public.rooms (
id uuid primary key default gen_random_uuid(),
code text unique not null check (char_length(code) = 6),
name text not null check (char_length(name) between 1 and 40),
passcode text not null check (char_length(passcode) between 6 and 40),
created_at timestamptz not null default now()
);
create table if not exists public.members (
id uuid primary key default gen_random_uuid(),
room_code text not null references public.rooms(code) on delete cascade,
nickname text not null check (char_length(nickname) between 1 and 20),
age_confirmed boolean not null default false,
terms_agreed_at timestamptz,
age_declared_at timestamptz,
joined_at timestamptz not null default now()
);
-- 既存の members テーブルがある場合の追加カラム(年齢の自己申告・同意日時)
alter table public.members add column if not exists age_confirmed boolean not null default false;
alter table public.members add column if not exists terms_agreed_at timestamptz;
alter table public.members add column if not exists age_declared_at timestamptz;
-- アバターのカスタマイズ(絵文字/画像・背景色・飾りをまとめたJSON文字列を保存)
alter table public.members add column if not exists avatar text;
create table if not exists public.messages (
id uuid primary key default gen_random_uuid(),
room_code text not null references public.rooms(code) on delete cascade,
nickname text not null check (char_length(nickname) between 1 and 20),
type text not null default 'text' check (type in ('text','image','video')), -- 'text' | 'image' | 'video'
content text check (content is null or char_length(content) <= 2000), -- テキスト本文(メディア時は空で可)
media_url text, -- media バケット内の公開URL
mod_status text not null default 'ok', -- 'ok'(通常表示) | 'removed'(運営者が削除)
mod_score numeric, -- 自動判定スコア(0〜1、目安)
view_once boolean not null default true, -- メディアは常に1回だけ表示(true固定)
deleted boolean not null default false, -- true=投稿者本人がソフトデリート済み(メンバーには表示しない)
deleted_at timestamptz, -- ソフトデリートした日時
deleted_by text, -- ソフトデリートした人(投稿者本人のニックネーム)
created_at timestamptz not null default now()
);
-- 既存テーブルがある場合の追加カラム
alter table public.messages add column if not exists mod_status text not null default 'ok';
alter table public.messages add column if not exists mod_score numeric;
alter table public.messages add column if not exists view_once boolean not null default true;
alter table public.messages add column if not exists deleted boolean not null default false;
alter table public.messages add column if not exists deleted_at timestamptz;
alter table public.messages add column if not exists deleted_by text;
-- 1回だけ表示の閲覧記録(メンバー=ニックネームごと。開いた人は二度と開けない判定に使う)
create table if not exists public.message_views (
id uuid primary key default gen_random_uuid(),
message_id uuid not null references public.messages(id) on delete cascade,
room_code text,
nickname text not null, -- 閲覧したメンバー(ニックネーム)
viewed_at timestamptz not null default now(),
unique (message_id, nickname)
);
create index if not exists message_views_room_idx
on public.message_views (room_code, nickname);
-- 通報・自動判定アラート(メタ情報のみ。メディア本体は入れない)
create table if not exists public.reports (
id uuid primary key default gen_random_uuid(),
room_code text,
room_name text,
message_id uuid,
reporter text, -- 通報者(自動判定の場合は '自動判定')
reported_nickname text, -- 投稿者名
content_type text, -- 'text' | 'image' | 'video'
kind text not null default 'report', -- 'report' | 'auto'
reason text,
score numeric, -- 自動判定スコア(通報時は null)
status text not null default 'pending', -- 'pending' | 'approved' | 'removed'
created_at timestamptz not null default now()
);
create index if not exists messages_room_idx
on public.messages (room_code, created_at);
-- 画像・動画の置き場(公開バケット:1ファイル10MBまで、対応MIMEのみ)
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
values ('media', 'media', true, 10485760,
array['image/jpeg','image/png','image/gif','image/webp','video/mp4','video/quicktime','video/webm','video/x-m4v'])
on conflict (id) do update
set file_size_limit = excluded.file_size_limit,
allowed_mime_types = excluded.allowed_mime_types;
-- ===== RLS(必要最小限:anon にはアプリの動作に必要な操作だけを許可)=====
-- rooms:入室・一覧のための select、部屋作成の insert、部屋閉鎖の delete のみ(update は許可しない)
-- members:参加の insert、モデレーション表示の select、退出処理の delete のみ(update は許可しない)
-- messages:表示の select、投稿の insert、モデレーション削除(状態更新)の update のみ(delete は許可しない)
-- reports:通報・アラートの insert、モデレーション表示の select、処理状態の update、部屋閉鎖に伴う delete のみ
-- message_views:1回だけ表示の記録に使う select / insert のみ(update / delete は許可しない)
-- storage.objects(media):表示の select、アップロードの insert、期限切れ削除の delete のみ(update は許可しない)
alter table public.rooms enable row level security;
alter table public.members enable row level security;
alter table public.messages enable row level security;
alter table public.reports enable row level security;
alter table public.message_views enable row level security;
drop policy if exists rooms_all on public.rooms;
drop policy if exists rooms_select on public.rooms;
create policy rooms_select on public.rooms for select to anon using (true);
drop policy if exists rooms_insert on public.rooms;
create policy rooms_insert on public.rooms for insert to anon with check (true);
drop policy if exists rooms_delete on public.rooms;
create policy rooms_delete on public.rooms for delete to anon using (true);
drop policy if exists members_all on public.members;
drop policy if exists members_select on public.members;
create policy members_select on public.members for select to anon using (true);
drop policy if exists members_insert on public.members;
create policy members_insert on public.members for insert to anon with check (true);
drop policy if exists members_delete on public.members;
create policy members_delete on public.members for delete to anon using (true);
drop policy if exists messages_all on public.messages;
drop policy if exists messages_select on public.messages;
create policy messages_select on public.messages for select to anon using (true);
drop policy if exists messages_insert on public.messages;
create policy messages_insert on public.messages for insert to anon with check (true);
drop policy if exists messages_update on public.messages;
create policy messages_update on public.messages for update to anon using (true) with check (true);
drop policy if exists reports_all on public.reports;
drop policy if exists reports_select on public.reports;
create policy reports_select on public.reports for select to anon using (true);
drop policy if exists reports_insert on public.reports;
create policy reports_insert on public.reports for insert to anon with check (true);
drop policy if exists reports_update on public.reports;
create policy reports_update on public.reports for update to anon using (true) with check (true);
drop policy if exists reports_delete on public.reports;
create policy reports_delete on public.reports for delete to anon using (true);
drop policy if exists message_views_all on public.message_views;
drop policy if exists message_views_select on public.message_views;
create policy message_views_select on public.message_views for select to anon using (true);
drop policy if exists message_views_insert on public.message_views;
create policy message_views_insert on public.message_views for insert to anon with check (true);
drop policy if exists media_read on storage.objects;
create policy media_read on storage.objects
for select to anon using (bucket_id = 'media');
drop policy if exists media_write on storage.objects;
create policy media_write on storage.objects
for insert to anon with check (bucket_id = 'media');
drop policy if exists media_delete on storage.objects;
create policy media_delete on storage.objects
for delete to anon using (bucket_id = 'media');
一度入室に成功した部屋は、その端末の中だけに「入室済み」として記録され、次回からは合言葉なしでワンタップで入れます。合言葉が必要なのは、その端末で初めて入る部屋だけです。この記録は端末の中だけに持ち、他の端末には引き継がれません(別のスマホやブラウザでは、初回は合言葉が必要です)。運営者によってメンバーから除外された場合は記録が消え、再度合言葉が必要になります。作成者が自分の合言葉を確認できる記録も、作成した端末の中だけに保存されます。
データベース 500MB、ファイル保存 1GB、転送 5GB までが無料枠の目安です。1週間アクセスがないとプロジェクトが一時停止します(データは消えません。ダッシュボードから再開できます)。動画が容量を一番使うので、このアプリでは動画を1本10MBまでに制限しています。
media バケットに1ファイル10MBまでと対応MIMEのみの制限を設定します。保存容量(1GB)を超えないよう、画像・動画には保持期間があります。初期設定は30日で、運営者は「運営者セットアップ」で 7日/30日/90日/無期限 から選べます。期限を過ぎたメディアは、アプリ起動時や部屋を開いたタイミングで自動的に削除され(Supabase 接続時は Storage の実ファイルと投稿のメディア参照を削除、ローカルプレビュー時は端末内データを削除)、チャット上では「期限切れで削除されました」という表示に置き換わります。テキストメッセージは容量をほぼ食わないため、初期設定では保持期間の対象外です(設定で一緒に消すことも選べます)。運営画面では現在のメディア件数・合計サイズの目安と「古いメディアを今すぐ削除」ボタンから、手動でまとめて空けることもできます。
送信者本人は、自分の投稿(テキスト・画像・動画)の下にある「削除」から、確認のうえで投稿を削除できます。他人の投稿を本人が削除することはできません(運営者によるモデレーション削除は従来どおりです)。削除はソフトデリートです:投稿データに deleted フラグと deleted_at / deleted_by が付くだけで、行そのものは消えません。メンバー側の部屋では、その投稿は「この投稿は削除されました」の表示に置き換わり、内容は見えなくなります。運営者の「投稿の確認」では、保持期間内は通常の投稿と同じ見た目で内容を確認でき、投稿者名の横に小さく「(投稿者削除)」とだけ表示されます。画像・動画のファイルも保持期間内は残り、保持期間を過ぎると従来の自動削除で消えます。Supabase 利用時は、セットアップSQLの再実行で deleted 関連カラムが追加されます。
メンバーが部屋を開くと、その時点で表示されている投稿が「既読」として記録されます(記録には message_views テーブルを流用します。お試しモードでは端末内の reads 記録)。自分の投稿には、誰かが読むと吹き出しの横に小さく「既読」と表示され、メンバーが3人以上の部屋では既読人数(例:既読 3)も出ます。1対1相当の部屋では「既読」だけの表示です。誰が読んだかの名前一覧は表示しません(人数だけです)。なお「1回だけ表示」の投稿は、実際に開いた時だけ既読・開封として記録されます。
既定ではニックネームの頭文字が小さく表示されます。表示アイコンの変更(絵文字/画像・背景色・飾り)は任意で、ロビーの表示名カード内の「ニックネーム変更」ボタンから開ける画面で、ニックネームの下にあります。入室時に選ぶ必要はありません。すでに設定済みの内容はそのまま表示に使われます。配色は既定のパステル系(ミント基調)に固定で、自分の吹き出しやボタンの色もその既定色で表示されます。
画像は自動で圧縮されます。動画は1本10MBまでです。画像・動画は保持期間(初期設定30日、運営者が7日/30日/90日/無期限から設定)を過ぎると自動で削除され、「期限切れで削除されました」と表示されます。投稿前に端末上で自動判定(目安)を行い、性的コンテンツの可能性が高い場合は運営者のモデレーション一覧にメタ情報のみのアラートが記録されます。他の人の画像・動画は本人の許可なく保存・外部へ共有しないでください。保存防止として、長押し・ドラッグ保存の無効化、動画のダウンロードボタン非表示、短時間だけ有効な表示用URLを使います。
通報:各メッセージ・画像・動画の下にある「通報」から、理由を選んで運営者へ知らせることができます。通報は reports テーブルにメタ情報だけが記録され、このページの「モデレーション」画面と、Supabase ダッシュボードの Table Editor(reports)で確認できます。
自動判定:画像と動画は投稿前に、投稿者の端末上だけで動く推論(TensorFlow.js + NSFW 判定モデルを CDN から読み込む方式)で判定されます。動画は数フレームを抜き出して判定します。画像データが判定のために外部サーバーへ送られることはありません。あくまで目安であり、誤判定があり得ます。
「可能性が高い」と判定された場合:投稿は通常どおり表示されます(保留表示はしません)。同時に、運営者へはアラートとしてメタ情報(部屋名・投稿者名・時刻・判定スコア)だけがモデレーション一覧に記録されます。判定された画像・動画そのものを運営者へ自動転送するコピーは作られません。問題がある場合は、モデレーション一覧から後追いでその投稿を削除できます。
違法なコンテンツ(とりわけ未成年が関わる疑い)のケース:疑わしい場合はダウンロード・転送をせず、速やかに削除のうえ、警察またはインターネット・ホットラインセンター(https://www.internethotline.jp/)へ相談・通報してください。この案内はモデレーション画面にも常設されています。違法行為や違法なコンテンツに関する通報を受けた場合は、警察その他の関係機関へ通報する場合があります(規約第10条)。
モデレーションの権限について:このページのモデレーション画面は、固定の運営用パスコードと秘密の質問の答えの両方(2重ロック)を入力しないと開けません。パスコードも答えもアプリ内で設定・変更できません。※ セットアップSQL の RLS ポリシーは、アプリの動作に必要な操作だけを許可する形になっています。公開してよいキーは anon key のみで、service_role キー(管理者キー)は絶対に公開・埋め込みしないでください。
最終的な権限は運営者の Supabase アカウントにあります。運営者はいつでも Supabase ダッシュボード(Table Editor / Storage)から全データの削除ができます。アプリ内モデレーションはその日常操作用の入口であり、権限の本体ではありません。
13歳未満の方はご利用いただけません(規約第9条)。独立した年齢確認チェックはなく、初回利用時の利用規約への同意をもって、利用者が13歳以上であることの確認を兼ねます。同意チェックは規約への同意の1つだけです。
※ この年齢確認は自己申告であり、公的書類などによる厳密な年齢確認ではありません。同意した日時と、規約同意に統合された年齢確認フラグは、この端末内に保存され、Supabase 接続時は部屋への参加記録(members テーブルの age_confirmed / terms_agreed_at / age_declared_at)にも規約同意の一部として記録されます。年齢について虚偽があったことが判明した場合は利用を停止します(規約第9条)。
画像・動画を選ぶと、送信前にプレビュー確認画面が開きます。写真・動画は常に「1回だけ表示」で送信されます(通常表示で送る選択はありません。テキストは対象外です)。メディアは、部屋では中身が見えないカバー(「タップして1回だけ表示」)で表示され、メンバーがタップして開くと、そのメンバーは二度と開けません。閉じた時点で、そのメンバーには「表示済み」と表示されます。
閲覧記録はメンバーごと(ニックネームごと)に保持します。お試し(ローカル)モードではこの端末内のデータ(views 記録)に、Supabase接続時は message_views テーブル(投稿ID×ニックネームで一意)に記録し、あわせてこの端末にも控えを残します。投稿の側には messages.view_once(メディアは常に true)が付きます。SQLは上のセットアップSQLに含まれています(既存の環境では追加の列とテーブルが if not exists で足されます)。
例外:投稿者本人は自分の投稿を、保持期間内は引き続き確認できます(規約第12条)。
部屋内の画像・動画には、カジュアルな保存を防ぐ仕組みが入っています。長押しで出る保存メニューとドラッグによる保存を無効化し、動画プレイヤーにはダウンロードボタンを表示しません。Supabase接続時は、ファイルを直接指す恒常的な公開URLを画面に渡さず、表示のたびに約10分だけ有効な署名付きURLを発行して使います(期限が切れたURLでは開けません)。お試し(ローカルプレビュー)では、メディアは端末の中だけで表示され、外部のURLは発行されません。
あわせて、利用規約第11条で「他の利用者の画像・動画を、本人の許可なく保存・外部へ共有しないこと」を明記しています。
※ 保存防止は「簡単に保存できなくする」ためのものです。
実運用では、保存しにくさ+規約の禁止+見つけた場合の対処(削除・利用停止)を組み合わせるのが現実的な対策です。すでに保持期間(初期設定30日)で古いメディアが自動削除される仕組みも、残り続けないという意味で効きます。このページには Web App Manifest(名前「鍵付き秘密基地チャット」、standalone 表示、テーマ色)とアイコン、モバイル用メタタグを埋め込んであるので、スマホのブラウザで開いて「ホーム画面に追加」をすれば、アプリのように全画面で起動できます。Google Play の登録料($25・1回だけ)も不要です。
制約の正直なところ:この配布形態(単一HTMLファイル)では Service Worker ファイルを別途置けないため、オフライン起動やインストールバナーの自動表示までは対応できません。対応しているのは Manifest とメタタグで可能な範囲(ホーム画面追加・全画面起動・テーマ色)までです。Cloudflare Pages などに置く際に sw.js を追加できる環境なら、フル PWA 化も可能です。
登録なしで使えます — まずはお試しから
下の「登録なしで試す」から、そのまま部屋を作って試せます。
理由を選んで送信してください。通報は運営者のモデレーション一覧に記録されます。通報したことは投稿者には通知されません。
運営者用の画面を開くには、運営用パスコードと秘密の質問の答えの両方を入力してください(2重ロック)。どちらか一方だけでは開けません。
名前はアプリ全体で共通の1つです。変えると、参加中の部屋すべての表示名も新しい名前になります(過去の投稿も含みます)。
任意の設定です。選ばない場合はニックネームの頭文字が小さく表示されます。
写真・動画は1回だけ表示で送られます。メンバーは1人1回だけ開けます(開いたらその人には「表示済み」と表示されます)。あなた(投稿者)は保持期間内は確認できます。