SupabaseのLocal Storageが、DB初期化で吹き飛んじゃうので、復元機能を作成してみた


# Supabaseによるローカル開発（概要）
Flutterでアプリケーションを作成を行っており、データベースとストレージしてSupabaseを使っています。

現在（2025/04/01）Supabaseは、
LAUNCHWEEKで新機能が発表されている最中です。（公式サイト）

https://supabase.com/launch-week

Supabaseにはローカル開発環境(※1)があるので、
常に実際のプロジェクト（クラウド上）にアクセスする必要はありません。

ローカルで立ち上げたDockerコンテナを用いて、データベースからのデータ取得やストレージからのファイル取得を行えます。

ローカルだけで作業を進められるのは、非常にありがたいことなんですが、
**データベースとStorageと組み合わせると、ちょっと困ったこと（改善できそうなこと）がでてきました。**

:::note info
**Supabase Storage**
文字通りですが、ストレージサービスです。

AWSのs3やAzureBlob Storageなど、ストレージサービスを利用されたことがある方は、イメージしやすいかと思います。

以下公式の情報をご確認ください。
:::

https://supabase.com/docs/guides/storage

※1：ローカル開発環境
Dockerコンテナを使って、データベースやストレージを用いたローカル開発ができます。

公式ドキュメント

https://supabase.com/docs/guides/local-development

以下は私の方で先日公開した記事です。こちらもよろしければご覧ください！

https://qiita.com/eno49conan/items/2169072ed6e4aa1d0164

## この先を読み進める前に
プログラミング初学者向けではありません。以下の内容についてご存知ではない場合、
読み進めるのが少し大変かもしれません。
- Dockerコンテナ
- データベース接続とSQL(Postgres)の基本（テーブル作成やデータ登録）
- S3などのクラウドストレージ操作
- javascriptなど何らかの言語の基本的な実装

## DB初期化でバケット自体が削除される

話を戻しますが、
困ったのは、データベースの初期化コマンドを実行したときです。
```shell
npx supabase db reset
```
`reset`という単語が使われているので、「初期化」としていますが、正確には再生成をするためのコマンドです。
実行すると、以下の処理を行います。
1. データベース自体を削除
1. マイグレーションファイル（SQLファイル）に沿って、テーブル作成やRLS定義を実施
1. `seed.sql`に定義したSQLで各テーブルへデータを投入

しかしStorageに関しては、
**バケットにアップロードしたファイル、バケット、設定したバケットポリシー（※2）**
これら全てが消えます。

以下画像は、初期化コマンドを実行前のローカル環境のストレージの中身です。

コマンドを実行すると、
バケット・アップロードファイル（青枠で囲った箇所）が消えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/d425614d-b50d-41d5-b354-bb309754de2d.png)

ポリシー（青枠で囲った箇所）も同様に消えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/d8fe55c7-f754-4c4b-83ad-b576d465707b.png)

DBの初期化、ってそんなにするの？となるかと思いますが、
それなりの回数は実行するかと思います。

DB側でテーブルを追加したり、カラムを追加・削除したりした後、これまでとの差分情報を取得します。（マイグレーションファイルの自動生成）

```shell 
npx supabase db diff --use-migra -f sample
```

このマイグレーションファイル作成コマンド実行後、
ターミナルに「`db reset`を実行して、問題がないか確認をしてください。」といった旨のメッセージが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/58e7a585-83b4-4722-a37b-c19200d4c99a.png)

DBを初期化して、
問題がないか（差分が正しく反映されるか）確認を行います。

**DB初期化の度に以下作業を行うのは大変です。**
いずれも増えるほど、全体の作業量が増えてしまいます。
- バケット作成
- ファイルアップロード
- バケットポリシーの適用（※2）

※2：バケットポリシーは、以下をご覧下さい。

https://supabase.com/docs/guides/storage/security/access-control

ネットで調べてみると、明確な方法は見当たらず、
スクリプトを作成してもらう対応になる、といったコメントも見つけました。

https://www.reddit.com/r/Supabase/comments/159m62m/is_it_possible_to_keep_the_bucket_in_local_env/

`config.toml`（設定ファイル）にも、設定できそうな箇所はありませんでした。
（ファイルの場所は後述のフォルダ構成で確認できます。）
```toml

[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"

# Image transformation API is available to Supabase Pro plan.
# [storage.image_transformation]
# enabled = truew

# Uncomment to configure local storage buckets
# [storage.buckets.images]
# public = false
# file_size_limit = "50MiB"
# allowed_mime_types = ["image/png", "image/jpeg"]
# objects_path = "./images"
```

ということなので、スクリプトを組んでみることにしました。
やってみると、
思っていたよりは短時間で実現することができたので、その内容を共有します！

ゴールは、
**DBの初期化時に、バケット・バケットの中身・バケットポリシーを復元できるようにすることです。**

## スクリプトを組む前に
#### 必要な情報
以下コマンドを実行してコンテナを起動します。
```shell
npx supabase start
```
コマンド実行後、以下内容がターミナルに出力されます。この情報を利用してスクリプト作成を進めます。
`S3`という文字が数か所ありますが、AWSのS3のことです。
```text
    GraphQL URL: http://127.0.0.1:54321/graphql/v1
  S3 Storage URL: http://127.0.0.1:54321/storage/v1/s3
          DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
      Studio URL: http://127.0.0.1:54323
    Inbucket URL: http://127.0.0.1:54324
      JWT secret: super-secret-jwt-token-with-at-least-32-characters-long
        anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ8.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ8.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
service_role key: eyJhbCgiOiJIUzI1NiLsInR5cCI1IkpKECJ1.eyJpc1MiOiJzdIQhGmFzZS1kKW1vIiwicm1sZOS1InNlcnZpY1Viwm1sZSIsImV1cCI1MTk4MzgxMjk5Nn0.EGKF31RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
   S3 Access Key: 929003a08b11bf1b5ff111c111f1d11c
   S3 Secret Key: 123456e4652dd093b7a15c58ae0d2d34bd487cc0ea1111aed6eda11111111111
       S3 Region: local
```

`anon key`と`service_role key`は以下をご確認ください。

https://supabase.com/docs/guides/api/api-keys#the-servicerole-key

:::note warn
**キーやURL情報の扱い**
ローカルでのみ利用可能な情報なので、特に隠さずに表示しています。（値自体も手を入れています）
実際の環境における各値の管理にはご注意ください。
:::

#### 必要な手順
ざっくりです。詳細は後述の通りです。
- 現在ストレージ上(バケット)にある各ファイルオブジェクトを取得
- バックアップフォルダへ配置
- データベースの初期化
- バケットの再作成
- バックアップフォルダからバケットへファイルアップロード
- バケットポリシーの適用

これらの処理を、3つのスクリプトに分けて進めることにしました。
javascriptで書きます。

図で示すと、以下の通りです。
左側に記載したファイル名で各スクリプトを作成します。（`xxx.js`の部分）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/5961699d-4b4e-46b0-8ee6-4d5fa36fe1f1.png)

## スクリプト作成と動作確認

#### フォルダ構成

先に最終的なフォルダの構成の情報を載せておきます。
javascriptを触ったことがある方は大きく疑問に感じる点はないと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/cc8b17d5-e8ef-4634-9af1-0e59691708a6.png)

- `scripts`フォルダ
  - 環境変数を記述した`.env`ファイル(`sample`はgit管理用)
  - 復元処理を実装したスクリプトファイル
  - バケットポリシー用のSQLファイル
- `supabase`フォルダ
  - `npx supabase init`実行時に自動生成されたフォルダで、画像にある通りマイグレーションファイルやデータ投入のための`seed.sql`などを配置
- `package.json`
　- 必要なライブラリやスクリプトを記載（復元処理のためのコマンドもここで定義） 

各ファイルの情報を記事の最後に載せました。
[こちら](https://gist.github.com/eno-conan/29189268f51bcae19725cdc9ddd6e40b)からも確認いただけます。

#### 必要になったライブラリ(`package.json`参照)
- `supabase`：supabaseのDB操作で必要なので、Storage関係の作業前からインストール済
- `@aws-sdk/client-s3`：javascriptでS3操作する際に必要
- `npm-run-all`：複数スクリプトを一度で実行する場合に必要
- `@supabase/supabase-js`：Supabaseのjavascript用SDK
- `mime-types`：バケット作成時に、アップロードを許可するMimeTypes(※3)を指定
- `pg`：SupabaseはPostgresで動いており、DB接続で必要

※3：MimeTypesが気になる方は以下をご確認ください。

https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/MIME_types/Common_types

#### バックアップ処理（`backupStorage.js`）
1. S3への接続
1. 対象バケットからオブジェクト取得
1. 2で取得したオブジェクトをローカルのフォルダに保存

（ここでデータベースの初期化が実行され、バケットも削除）

#### 復元処理（`restoreStorage.js`）
1. バケット作成
    1. `public`設定はfalse(※4)
    2. アップロード可能なMimeTypesを`['image/png']`で設定（他は特に必要なし）
    3. 最大サイズを10MBに設定
2. バックアップにある各ファイルをアップロード
    1. バックアップ時にフォルダ構成もバケットの状態を復元しています。

※4：`public`、`private`の違いは以下参照

https://supabase.com/docs/guides/storage/buckets/fundamentals#public-buckets

#### ポリシー適用処理（`applyBucketPolicy.js`）
1. ローカルのデータベースに接続
1. javascriptファイルと同階層に配置したSQLファイルを読み込み、ファイル内容をバケットに対して適用

#### スクリプト実行
以下のように、`package.json`のscriptで記載した通りです。
```json:package.json
{
  "devDependencies": {},
  "scripts": {
    "start": "supabase start",
    "stop": "supabase stop",
    "db:reset": "run-s db:backup:storage db:do:reset db:restore:storage db:restore:policy",
    "db:do:reset": "supabase db reset",
    "db:backup:storage": "node scripts/backupStorage.js",
    "db:restore:storage": "node scripts/restoreStorage.js",
    "db:restore:policy": "node scripts/applyBucketPolicy.js"
  },
  "dependencies": {}
}
```
以下コマンドを実行して、復元まで行うことができました。
```shell
npm run db:reset
```

#### 動作確認
以下はコマンドを実行したときのマネジメントコンソールの動画です。
実際は1分ほどかかりますが、中間はカットしました。

![qiita_supabase_storage_restore.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/17bc1c9e-864c-4f6f-8f90-053cb55dcdc7.gif)

はじまってすぐに、動画右上にエラーっぽいダイアログが表示されます。コンテナの初期化が開始され、アクセスできない状態に入ります。

その後、下部中央あたりに黄色のダイアログで、バケットにアクセスできません、と表示されます。

（ここで実際は30秒ほど待ちます）

最後にリロードをして、バケットに画像がアップロードされていることを確認できます。
画像を確認できたので、復元できていることになります。

## 起こりうるケースに対応したスクリプトへ

#### DB初期化時のデータ投入SQLでエラーが発生してしまった

図で書くと以下の通りです。以降は同じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/625bca6f-9190-432a-b0f6-aa88ad7f2802.png)

発生した場合、バックアップは存在し、バケットが存在しない状態になります。
このまま再度処理を実行すると、バケットが存在しないため、その段階でエラーが発生します。

バケットが存在しないエラーをcatchし、既に作成済のバックアップを適用するか、ユーザ入力を求める形にしました。
`y`を入力した場合は、この処理は正常終了扱いとして、DB初期化に進み、
`n`とした場合は、エラーで終了するようにしました。

#### 間違ってストレージのファイルを消してしまった

図で言えば、以下処理だけ実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/a9a9d145-a734-4b1e-b7be-d356ba29b69c.png)

バックアップがない初回のタイミングでやってしまった場合は、どうしようもありませんが、
一度バックアップを作成済であれば、復元可能です。

`package.json`には記載してありますが、
以下コマンドを実行すれば復元処理だけ実行可能です。
```shell
npm run db:restore
```

## まとめ
今回はSupabase DatabaseとStorage、
両方を用いたローカル開発を進めやすくする仕組みを作ることができました。

作業中は適宜、
ClaudeやDeepSeekなどに助けてもらいながら進めました。
方向性が大きくずれることが少ないので、思っていたよりも短い時間で進めることができました。

Supabaseをこの後もさわる機会が多いので、
新たにまとめられそうな情報があれば、まとめたいと思います。

## 実装詳細

https://gist.github.com/eno-conan/29189268f51bcae19725cdc9ddd6e40b