Supabase専用端末を作ったときの備忘録

以前使っていたノートPCを掘り起こし、Supabase専用端末化してみたときの備忘録です。

※画像やコンソール出力をぺたぺた貼ってたらすごい縦に長くなってしまったので、見出し以下について`<details></details>`で折りたたんでみました。

## 環境情報

### PC1（Supabase用）

<details>
<summary>詳細</summary>

|項目|内容|
|---|---|
|PC|ThinkPad X1 Carbon (2018モデル)|
|OS|Windows10 Home ⇒ Windows11 Home (23H2)|
|CPU|i7-8550U|
|メモリ|16GB|
|Docker Engine|26.0.0|
|Docker Desktop|4.29.0|
|docker compose|v2.26.1-desktop.1|
|scoop|v0.4.1|

- 2018年頃に購入したPCです
- wsl2、Ubuntu22.04導入済み（今回は使わなかった）
- [scoop](https://scoop.sh/)導入済み
- 今回の作業を行う前にWindows11にアップデート、Docker（wsl）は再インストール、scoopは最新にアップデートしました

</details>

### PC2（アプリケーション開発用）

<details>
<summary>詳細</summary>

|項目|内容|
|---|---|
|PC|HP Spectre x360|
|OS|Windows11 Pro|
|CPU|i7-1165G7|
|メモリ|16GB|
|Docker Engine|25.0.3|
|Docker Desktop|4.28.0|
|docker compose|v2.24.6-desktop.1|
|Node.js|v20.13.1|
|npm|10.5.2|

- 2022年頃に購入したPCです
- wsl2、Ubuntu20.04導入済み
- wsl2にNode.jsを直インストール済み

```:nodejsインストール
$ curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
$ sudo apt-get install -y nodejs
```

</details>

### その他

<details>
<summary>詳細</summary>

|項目|内容|
|---|---|
|無線LANルーター|Aterm WG2600HP4|
|サブネットマスク|255.255.255.0|
|デフォルトゲートウェイ|192.168.10.1|

- 後の作業内容としてSupabase用PCのIPアドレスの固定化を行うので関係してそうな箇所を記載しました

</details>

## セットアップ
`以後、見出しにPC1と書いているものはSupabase用PCでの作業内容、PC2と書いているものはアプリケーション開発用PCでの作業内容となります`

### scoop から supabase CLI をインストール (PC1)

<details>
<summary>詳細</summary>

Windows用の supabase CLIをインストールします

```bat:cmd(管理者)
> scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
> scoop install supabase
> scoop list
Installed apps:

Name     Version Source Updated             Info
----     ------- ------ -------             ----
7zip     23.01   main   2024-05-12 14:12:00
hadolint 2.10.0  main   2022-08-11 19:25:59
supabase 1.165.0 main   2024-05-12 14:12:01
```

- supabase CLIについて1.165.0というバージョンがインストールできました
- supabaseのインストールと併せて7zipがインストールされました（たぶん）
- hadolintは今回の記事の内容とは一切関係ありません
- ちなみに管理者として起動したコマンドプロンプトにて実行しました（不要かも）

</details>

### supabaseのローカル環境起動 (PC1)

<details>
<summary>詳細</summary>

```bat:cmd
> cd /D C:\workspace_supabase
> mkdir example-app-backend
> cd example-app-backend
> supabase init
Generate VS Code settings for Deno? [y/N] N
Generate IntelliJ Settings for Deno? [y/N] N
Finished supabase init.

> dir supabase /S /B
C:\workspace_supabase\example-app-backend\supabase\config.toml
C:\workspace_supabase\example-app-backend\supabase\seed.sql
```

- 今回は`C:\workspace_supabase\example-app-backend`ディレクトリ内にsupabaseのプロジェクトの作成を行いました
- `supabase init`コマンドを実行したディレクトリ内に1フォルダ、2ファイルが生成されました


続けてstartコマンドを実行
```bat:cmd
> supabase start
15.1.1.41: Pulling from supabase/postgres
17d0386c2fff: Pull complete
ec8061954605: Pull complete
e54d94900333: Pull complete

~~~省略~~~

Seeding data supabase\seed.sql...
2.8.1: Pulling from supabase/kong
213ec9aee27d: Already exists

~~~省略~~~

Started supabase local development setup.

         API URL: http://127.0.0.1:54321
     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: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
   S3 Access Key: 625729a08b95bf1b7ff351a663f3a23c
   S3 Secret Key: 850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
       S3 Region: local
```

- 初回はstartコマンド実行時にコンテナを起動するためのイメージのプルが行われるため数分掛かりました
- startコマンド実行時にコンソールに表示される各種情報はアプリケーション開発の際、外からsupabaseに接続するのに必要となります
  - supabaseのローカルの情報は`supabase status`コマンドから再度確認できるみたいなので控えておかなくても大丈夫かも？

---
起動したコンテナについて確認しました
```bat:cmd
> docker ps
CONTAINER ID   IMAGE                                             COMMAND                   CREATED         STATUS                   PORTS                                              NAMES
48087ba9578d   public.ecr.aws/supabase/studio:20240422-5cf8f30   "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes (healthy)   0.0.0.0:54323->3000/tcp                            supabase_studio_example-app-backend
dfd206489b0b   public.ecr.aws/supabase/postgres-meta:v0.80.0     "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes (healthy)   8080/tcp                                           supabase_pg_meta_example-app-backend
d0b516eb993a   public.ecr.aws/supabase/edge-runtime:v1.45.2      "sh -c 'mkdir -p /ho…"   2 minutes ago   Up 2 minutes             8081/tcp                                           supabase_edge_runtime_example-app-backend
5ded325e82a4   public.ecr.aws/supabase/imgproxy:v3.8.0           "imgproxy"                2 minutes ago   Up 2 minutes (healthy)   8080/tcp                                           supabase_imgproxy_example-app-backend
fdec91fa0e1d   public.ecr.aws/supabase/storage-api:v1.0.6        "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes (healthy)   5000/tcp                                           supabase_storage_example-app-backend
9eeca099c0be   public.ecr.aws/supabase/postgrest:v12.0.1         "/bin/postgrest"          2 minutes ago   Up 2 minutes             3000/tcp                                           supabase_rest_example-app-backend
711583b04956   public.ecr.aws/supabase/realtime:v2.28.32         "/usr/bin/tini -s -g…"   2 minutes ago   Up 2 minutes (healthy)   4000/tcp                                           supabase_realtime_example-app-backend
f30fe6d1f89d   public.ecr.aws/supabase/inbucket:3.0.3            "/start-inbucket.sh …"   2 minutes ago   Up 2 minutes (healthy)   1100/tcp, 2500/tcp, 0.0.0.0:54324->9000/tcp        supabase_inbucket_example-app-backend
966eb30b1803   public.ecr.aws/supabase/gotrue:v2.149.0           "auth"                    2 minutes ago   Up 2 minutes (healthy)   9999/tcp                                           supabase_auth_example-app-backend
cf1802ff839a   public.ecr.aws/supabase/kong:2.8.1                "sh -c 'cat <<'EOF' …"   2 minutes ago   Up 2 minutes (healthy)   8001/tcp, 8443-8444/tcp, 0.0.0.0:54321->8000/tcp   supabase_kong_example-app-backend
20c7522df8ba   public.ecr.aws/supabase/postgres:15.1.1.41        "sh -c 'cat <<'EOF' …"   2 minutes ago   Up 2 minutes (healthy)   0.0.0.0:54322->5432/tcp                            supabase_db_example-app-backend
```

---
ちなみにSupabaseのDocker環境は以下のコマンドから停止できます
```bat:cmd
> supabase stop
Stopped supabase local development setup.
Local data are backed up to docker volume. Use docker to show them: docker volume ls --filter label=com.supabase.cli.project=example-app-backend
```
- 環境を丸ごと削除したいときは`--no-backup`オプションをつけて実行することでボリュームを含むコンテナ環境の破棄ができます

</details>

### IPアドレスを固定化 (PC1)

<details>
<summary>詳細</summary>

接続される側のローカルIPアドレスが変わってしまうと手間なのでおおよそ以下の手順で固定化しました。

`ipconfig`で自動割り当て中の設定を確認

```bat:cmd
> ipconfig
Wireless LAN adapter Wi-Fi:
~~~省略~~~
   IPv4 アドレス . . . . . . . . . . . .: 192.168.10.104
   サブネット マスク . . . . . . . . . .: 255.255.255.0
   デフォルト ゲートウェイ . . . . . . .: 192.168.10.1 
```

設定 > ネットワークとインターネット > Wi-Fi > [Wi-FiのSSID名] と進み IP割り当ての「編集」をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/13f83251-1850-62e1-b4e7-543597827a45.png)

自動⇒手動に切り替え、IPv4のスイッチをオンに切り替える。
その後、事前に確認した内容を元に設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/4b32f752-3fe1-d866-c404-4a0c6c0c7f4d.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/671e7ffe-dc1f-8475-117d-6a2920a9219e.png)
↓
今回の私の環境では以下のような設定となりました

|項目|内容|
|---|---|
|IPv4|オン|
|IPアドレス|192.168.10.104|
|サブネットマスク|255.255.255.0|
|ゲートウェイ|192.168.10.1|
|優先DNS|192.168.10.1|
|HTTPS経由のDNS|オフ|
|IPv6|オフ|

</details>

### 接続確認 (PC2)

<details>
<summary>詳細</summary>

ブラウザでSupabaseのStudioにアクセスしダッシュボードが表示されることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/89893e04-de06-4720-55b0-1f42814fbe28.png)

- 具体的には http://192.168.10.104:54323/ にアクセスします
  - PC1側で`supabase start`の完了時に表示されていた`Studio URL`を基に、IPアドレス部分（`127.0.0.1`）をPC1のローカルIP（`192.168.10.104`）に書き換えています

---
[Inbucket](https://inbucket.org/)というローカル環境用の仮想メールサーバーのGUIにアクセスできることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/13f0817c-f488-0a69-f833-a5459c2cd895.png)

- 具体的には http://192.168.10.104:54324/ にアクセスします
  - PC1側で`supabase start`の完了時に表示されていた`Inbucket URL`を基に、IPアドレス部分（`127.0.0.1`）をPC1のローカルIP（`192.168.10.104`）に書き換えています

---
続けてSQLクライアントソフト（今回はA5M2）で接続できることを確認します
こちらも`supabase start`の完了時に表示されていた`DB URL`の情報を分解し、IPアドレス部分だけ書き換えることで接続できました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/75fbfbe7-9e3b-b147-5431-16ff3c72372d.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/cc6a5bb0-9c5d-aef5-5dab-4db014c4eaac.png)

</details>

## デモアプリ開発 (PC1,PC2)
- 今回は認証とサインアップとアプリ固有のテーブルを作る辺りが問題なく行えることを確認します
- 具体的にはNuxt3を使用したTODOリストを管理するアプリケーションを作成します

### TODOリスト情報を管理するtasksテーブルを作成 (PC2)

<details>
<summary>詳細</summary>

Studioの`Table Editor`画面を開き `Create a new table`ボタンをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/b57c170e-032d-a119-8f84-b3650b2ab566.png)

右から出てくるサイドバーでtasksテーブルの情報を入力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/e5312443-7945-2a0b-b14a-8e836458b3f0.png)
- `Enable Row Level Security`はチェックを入れておく（デフォルトでチェックされてるかも）

---
同サイドバー内でカラム情報を入力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/39a13252-f0a5-17b2-bb99-0e6d2a4e3053.png)

|カラム名|型|デフォルト|備考|
|---|---|---|---|
|id|int8|null|`Primary`と`Is Identity`にチェック入れる|
|title|text|null|`Is Nullable`にチェック入れる|
|completed|bool|false|(NOT NULL)|
|user|uuid|auth.uid() ※1|(NOT NULL)|
|created_at|timestamptz|now()|(NOT NULL)|

※1 `auth.uid()`はauthスキーマのusersテーブルから認証中のユーザーIDを取得するというSupabaseが用意する関数です
- [Row Level Security | Supabase Docs](https://supabase.com/docs/guides/database/postgres/row-level-security#authuid)

---
外部キーの情報は特に入力せずに、`Save`ボタンをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/a4eb7c5b-946c-6e35-2edc-0ef9ecf81d3f.png)

---
tasksテーブルの作成が行えました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/976dcbe8-f615-87a0-b652-b9f1898da739.png)

---
tasksテーブル横の「...」より`View Policies`をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/d07a4c12-e52d-b678-3db5-cd52dfc04176.png)

---
tasksテーブルに対して「RLSが有効化されているがポリシーが存在しない」というメッセージが表示されています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/6bbefcfa-a329-4273-b0b3-9acdce7a35e7.png)

---
RLSは↑のポリシーの一覧画面から`Create a new policy`ボタンをクリックした先のGUIでも作成できますが、今回は`SQL Editor`の画面でクエリをペーストしての設定を行います。サイドバーからSQL Editorページに遷移

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/e52b76e6-a001-6412-5efe-e03a7016c0be.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/b114adfd-302b-c957-d831-984a891374a1.png)

---
以下のクエリをクエリ欄にペーストし、`Run`ボタンをクリック
```sql:tasksテーブルのRLSポリシー
create policy "Individuals can create tasks." on tasks for
    insert with check (auth.uid() = user);
create policy "Individuals can view their own tasks. " on tasks for
    select using ((select auth.uid()) = user);
create policy "Individuals can update their own tasks." on tasks for
    update using ((select auth.uid()) = user);
create policy "Individuals can delete their own tasks." on tasks for
    delete using ((select auth.uid()) = user);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/0ddf43eb-083c-af18-e1b9-28927bd81aeb.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/ebc45a5f-4656-15bb-91a4-9b0cc329250f.png)

- エラーっぽいトーストメッセージが表示されいますが、クエリ自体は正しく実行できてそうなログが出ました
- ちなみに↑のクエリはINSERT、DELETE、UPDATE、SELECTのそれぞれについて認証中のユーザーの自身に紐づく情報のみ操作（登録、削除、更新、取得）可能とするRLSです

---
tasksテーブルのポリシーを確認するページに戻るとINSERT、DELETE、UPDATE、SELECTのそれぞれについてRLSが設定されていることが確認できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/42ccabc4-356f-f5f7-1238-a38ac8ac3ed8.png)

---
A5M2の方からもpublicスキーマ内にtasksテーブルの存在を確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/f666c63d-606c-cb5b-e1ba-59cd2dd668d1.png)

</details>

### Supabaseのダンプを取得 (PC1)

<details>
<summary>詳細</summary>

supabace CLIの`db dump`コマンドを使用して↑の手順で作成したtasksテーブルに関する定義を含むダンプを取得

```bat:cmd
> supabase db dump --local --file supabase/seed.sql
Dumping schemas from local database...
Dumped schema to supabase/seed.sql.
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/79d5fbff-d191-7e4d-2a2e-c2cdf217615a.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/e2849c80-f96c-111a-37f5-a8948a9a0843.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/40a31bb5-4e2b-de58-5db2-53107a5ef291.png)

- PC2側でブラウザを経由して作成したtasksテーブルに関する情報を含むダンプが取得できることを確認できました

</details>

### Nuxt3のアプリケーション作成1 (PC2)

- Nuxt3の新規プロジェクトを作成しプロジェクト内で利用するモジュールを導入するとこまで実施します

<details>
<summary>詳細</summary>

- Nuxt3からSupabaseを利用するためのモジュール（[@nuxtjs/supabase](https://supabase.nuxtjs.org/)）があるのでこちらを使用します
  - 上記モジュールの[デモとしてTODOリストを管理するアプリケーションのコードが公開されている](https://github.com/nuxt-modules/supabase/tree/main/demo)のでこちらを参考にしつつ一部書き換えるなどしました
- UIを作るのに[PrimeVue](https://primevue.org/)を使いたいので、Nuxt3向けのモジュール（[nuxt-primevue](https://github.com/primefaces/primevue-nuxt-module)）を利用します
  - 併せて[primeflex](https://github.com/primefaces/primeflex)、[primeicons](https://github.com/primefaces/primeicons)もインストールします
- Dockerは使わずwsl2(Ubuntu)に直インストールしているNode.jsよりプロジェクトを作成します

```bash:wsl2
$ cd ~/
$ mkdir example-app-frontend && cd $_
$ npx nuxi@latest init .
$ npm install -D @nuxtjs/supabase supabase
$ npm install -D nuxt-primevue primeflex primeicons sass
```

package.jsonの中身は以下の通りとなりました
```json:package.json
{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "nuxt": "^3.11.2",
    "vue": "^3.4.27",
    "vue-router": "^4.3.2"
  },
  "devDependencies": {
    "@nuxtjs/supabase": "^1.2.2",
    "nuxt-primevue": "^0.3.1",
    "primeflex": "^3.3.1",
    "primeicons": "^7.0.0",
    "sass": "^1.77.1",
    "supabase": "^1.165.0"
  }
}
```

プロジェクトのルートに`.env`を作成
```.env
SUPABASE_URL="http://192.168.10.104:54321"
SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
```

インストールしたモジュールの設定をnuxt.config.tsに追加
```diff_typescript:nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
-  devtools: { enabled: true }
+  devtools: { enabled: true },
+
+  modules: [
+    '@nuxtjs/supabase',
+    'nuxt-primevue'
+  ],
+
+  supabase: {
+    redirectOptions: {
+      login: '/',
+      callback: '/confirm',
+    },
+  },
+
+  primevue: {
+    usePrimeVue: true,
+    components: {
+      include: '*',
+    },
+    directives: {
+      include: '*',
+    },
+    composables: {
+      include: '*',
+    },
+  },
+
+  css: [
+    'primevue/resources/themes/mira/theme.css',
+    'primeflex/primeflex.css',
+    'primeicons/primeicons.css',
+  ],
})
```
- primevueに含まれるテーマCSS、primeflexとprimeiconsのcssも追加

---
Nuxt3の開発サーバーを起動

```bash:wsl2
$ npm run dev

> dev
> nuxt dev

Nuxt 3.11.2 with Nitro 2.9.6                                                                                                                                                       9:43:52 PM
                                                                                                                                                                                   9:43:53 PM
  ➜ Local:    http://localhost:3000/
  ➜ Network:  use --host to expose

  ➜ DevTools: press Shift + Alt + D in the browser (v1.3.1)                                                                                                                        9:43:54 PM

ℹ Vite client warmed up in 2702ms                                                                                                                                                 9:43:58 PM
ℹ Vite server warmed up in 3409ms                                                                                                                                                 9:43:59 PM
✔ Nuxt Nitro server built in 2281 ms                                                                                                                                        nitro 9:44:01 PM
```

---
ブラウザで http://localhost:3000/ にアクセス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/9cbf7c57-82cb-34d0-478f-16a92ee6acba.png)

- Nuxt3のWelcomeページが見れることを確認
- レイアウトが少し崩れてるのはPrimeVueのテーマCSSとかを読み込んでいることが関係しているかもしれません（このページは後の手順で消すので問題なし）

</details>

### TypeScript用の型情報を生成 (PC2)

<details>
<summary>詳細</summary>

npmでインストールしたsupabase CLIの`gen types typescript`コマンドよりTypeScript用の型情報を生成します
```bash:wsl2
$ mkdir types
$ npx supabase gen types typescript --db-url postgresql://postgres:postgres@192.168.10.104:54322/postgres > types/database.types
Connecting to 192.168.10.104 54322
v0.80.0: Pulling from supabase/postgres-meta

~~~省略~~~

Status: Downloaded newer image for public.ecr.aws/supabase/postgres-meta:v0.80.0
(node:1) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/8e8acdb5-a3b1-235c-5d95-b9573a3279e8.png)

- tasksテーブルに関する情報を含む型情報が生成されました
- `--db-url`オプションにはPostgreSQLへの接続文字列を指定します
  - PC1側で`supabase start`の完了時に表示されていた`DB URL`を基に、IPアドレス部分（`127.0.0.1`）をPC1のローカルIP（`192.168.10.104`）に書き換えています

</details>

#### (不要) TypeScript用の型情報をPC1側で生成 (PC1)

<details>
<summary>詳細</summary>

- 最初はPC1側で`--local`オプション付きで以下のようなコマンドを実行して型情報を生成し、USBメモリ経由でPC2側にコピーという方法をとっていました。

```bat:cmd
> mkdir types
> supabase gen types typescript --local > types\database.types.ts
Connecting to db 5432
(node:1) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
```

- 実際には [TypeScript用の型情報を生成 (PC2)](#typescript用の型情報を生成-pc2) に記載した通り `--db-url` オプション付きで実行することでPC2側で`gen types typescript`コマンドを実行できるため不要な手順であることがわかりました。

</details>

### Nuxt3のアプリケーション開発2 (PC2)

- 型情報の生成後の続きとして各画面の開発を実施します

<details>
<summary>詳細</summary>

app.vueを以下の通り書き換え
```diff_vue:app.vue
<template>
-  <div>
-    <NuxtWelcome />
-  </div>
+  <ClientOnly>
+    <Toast />
+  </ClientOnly>
+  <Body class="m-0">
+    <div class="w-screen h-screen surface-ground">
+      <NuxtPage />
+    </div>
+  </Body>
</template>
```

ログイン画面を新規作成
```vue:pages/index.vue
<template>
  <div class="flex align-items-center justify-content-center h-full flex-wrap">
    <Card class="flex justify-content-center w-20rem">
      <template #title>
        <div class="text-center">
          <SelectButton
            v-model="state.authType"
            :allow-empty="false"
            :options="authTypes"
            :pt="{
              button: () => ({
                class: 'w-7rem p-button-sm'
              })
            }"
          />
        </div>
      </template>
      <template #content>
        <Password v-model="state.dummyPassword" class="hidden"/>

        <label class="block w-full">Email</label>
        <InputText v-model="state.email" />

        <label class="mt-2 block w-full">Password</label>
        <Password v-model="state.password" />
      </template>
      <template #footer>
        <Button
          :label="state.authType === 'login' ? 'Login' : 'Signup'"
          class="w-full"
          :disabled="!state.email || !state.password"
          @click="execLoginOrSignup"
        />
      </template>
    </Card>
  </div>
</template>

<script setup lang="ts">
const toast = useToast()

const state = reactive<{
  authType: 'login' | 'signup'
  email: string
  password: string
  dummyPassword: string
}>({
  authType: 'login',
  email: '',
  password: '',
  dummyPassword: ''
})

const authTypes = ['login', 'signup']

const user = useSupabaseUser()
const { auth } = useSupabaseClient()

const execLoginOrSignup = async () => {
  if (state.authType === 'login') {
    const { error } = await auth.signInWithPassword({
      email: state.email,
      password: state.password,
    })
    if (error) {
      toast.add({
        severity: 'error',
        summary: 'Login error',
        detail: error.message,
        life: 5000,
      })
    }
  } else {
    const { error } = await auth.signUp({
      email: state.email,
      password: state.password,
    })
    if (error) {
      toast.add({
        severity: 'error',
        summary: 'Signup error',
        detail: error.message,
        life: 5000,
      })
    }
  }
}

watchEffect(() => {
  if (user.value) {
    navigateTo('/tasks')
  }
})
</script>
```

- `nuxt.config.ts`の`supabase.redirectOptions.login`で設定したログインページのパスと揃える必要があります

TODOリストの管理画面を作成
```vue:pages/tasks.vue
<template>
  <div class="flex align-items-center justify-content-center w-full py-4 flex-wrap">
    <Card class="flex justify-content-center w-30rem">
      <template #title>
        <div class="text-3xl">
          Todo List.
        </div>
      </template>
      <template #content>
        <div class="grid">
          <div class="col-12">
            <div class="grid align-items-center">
              <div class="col">
                <InputText v-model="state.newTask" class="w-full" />
              </div>
              <div class="col-fixed">
                <Button icon="pi pi-plus" label="Add" size="small" @click="addTask" />
              </div>
            </div>
          </div>
          <div class="col-12">
            <template v-if="tasks">
              <DataView :value="tasks" data-key="id">
                <template #list="slotProps">
                  <div class="grid grid-nogutter">
                    <div v-for="(task, index) in slotProps.items" :key="index" class="col-12">
                      <div class="grid">
                        <div class="col">
                          {{ task.title }}
                        </div>
                        <div class="col-fixed">
                          <div class="flex align-items-center justify-content-between flex-wrap">
                            <div class="flex mr-2">
                              <InputSwitch v-model="task.completed" @change="completeTask(task as Task)" />
                            </div>
                            <div class="flex">
                              <Button icon="pi pi-trash" severity="danger" size="small" @click="removeTask(task as Task)" />
                            </div>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </template>
                <template #empty>
                  <div>
                    No tasks registered
                  </div>
                </template>
              </DataView>
            </template>
          </div>
        </div>
      </template>
    </Card>
  </div>
</template>

<script setup lang="ts">
import type { Database } from '~~/types/database.types'

interface Task {
  id: number
  title: string
  completed: boolean
  user?: string
  created_at?: string
}

const client = useSupabaseClient<Database>()
const user = useSupabaseUser()

const state = reactive({
  newTask: '',
  loading: false
})

const { data: tasks } = await useAsyncData('tasks', async () => {
  const { data } = await client.from('tasks').select('id, title, completed').eq('user', user.value!.id).order('created_at')

  return data
})

async function addTask() {
  if (state.newTask.trim().length === 0) return

  state.loading = true

  const { data } = await client.from('tasks')
    .upsert({
      user: user.value!.id,
      title: state.newTask,
      completed: false,
    })
    .select('id, title, completed')
    .single()

  tasks.value?.push(data!)
  state.newTask = ''
  state.loading = false
}

const completeTask = async (task: Task) => {
  await client.from('tasks').update({
    completed: task.completed,
  }).match({
    id: task.id,
  })
}

const removeTask = async (task: Task) => {
  tasks.value = tasks.value?.filter(t => t.id !== task.id) ?? []
  await client.from('tasks').delete().match({
    id: task.id,
  })
}
</script>
```

- この画面は`nuxt.config.ts`の`supabase.redirectOptions.exclude`で除外URLとして設定していないため認証が必須として扱われます
- 認証を済ませていない状態でpages/tasks.vueに直アクセスした場合、モジュール内のルートミドルウェア処理によって`supabase.redirectOptions.login`で指定したパスに遷移します

</details>

### デモアプリの動作確認 (PC2)

<details>
<summary>詳細</summary>

`nuxt dev`で起動する開発用のサーバーが起動している状態で http://localhost:3000/ にアクセス

pages/index.vueのログイン＆サインアップ画面が表示できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/d5fe6ac5-339a-ac99-b1a3-531c890011fe.png)

---
存在しないユーザーを入力しログインを試みた状態でSupabaseの認証結果としてエラーを受け取り、トーストメッセージとして表示できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/80dd47b7-d95c-fe84-f684-5fb338f0037f.png)

---
サインアップを実行 (Emailに`testuser@example.com`、Passwordに`password`)と入力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/3c5e5963-93a9-71f3-0789-055a07432d26.png)

---
Studioの`Authentication > Users`画面で登録したサインアップしたユーザーが存在することを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/109ad811-3bcd-b004-73b8-95602888d29a.png)

---
サインアップ成功後、ログイン＆サインアップの画面コンポーネント内のwatchEffectの処理によりTODOリストの管理画面に遷移したことを確認

```vue:pages/index.vue (抜粋)
<script setup lang="ts">
const user = useSupabaseUser()

watchEffect(() => {
  if (user.value) {
    navigateTo('/tasks')
  }
})
</script>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/8c0c2465-bf48-9961-4e2b-4f501efd0c19.png)

---
TODOリストの管理画面で1件のタスクを登録できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/6c27aa00-387f-f1b9-1c10-a01e47497783.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/914ae067-4073-10c3-df9f-bf457a1535b1.png)

---
Studioの`Table Editor`画面でtasksテーブルのデータとして登録した内容が閲覧できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/cb8c3974-a9cb-1109-f071-ba085babe81c.png)

---
TODOリストの管理画面で1件のタスクを更新できることを確認（スイッチをONにする）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/71353599-eea7-3803-27ac-19b684b4c1b9.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/d3a73997-1cba-f958-459a-ac67aab29c9d.png)

---
TODOリストの管理画面で1件のタスクを削除できることを確認（ゴミ箱ボタンクリック）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/386db519-511c-79e3-6f80-639e2d4d1cf5.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/2891d2c6-975f-ded4-71b9-d705bcdd31ef.png)

</details>

## Supabaseのバックアップを含まないプロジェクトの破棄 (PC1)
せっかくなので`--no-backup`を付けたstopコマンドを実行してみました

<details>
<summary>詳細</summary>

```bat:cmd
> supabase stop --no-backup
Stopped supabase local development setup.
```

ボリュームが空になっていることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/aaed91b0-7905-fe26-da2d-1823e3d07126.png)

</details>

## 再度Supabaseのローカル環境立ち上げ (PC1)
バックアップ済みの`supabase/seed.sql`を元にtasksテーブルとtasksテーブルのRLSポリシーが作成されることを確認します

<details>
<summary>詳細</summary>

```bat:cmd
> supabase start
Seeding data supabase\seed.sql...
Started supabase local development setup.

         API URL: http://127.0.0.1:54321
     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: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
   S3 Access Key: 625729a08b95bf1b7ff351a663f3a23c
   S3 Secret Key: 850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
       S3 Region: local
```

---
PC1側でブラウザからStudioの`Table Editor`画面にアクセス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/b300bfcc-1447-d67b-e267-74aaeb395679.png)

- tasksテーブルが出来上がっていることが確認できました

---
続けてRLSのポリシーを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/02012796-e47d-c203-fa48-903a07f33766.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/006d3973-0277-7caf-2a3b-abd59a9564fa.png)

- INSERT、DELETE、UPDATE、SELECTのそれぞれのRLSポリシーについて以前の状態が復元できていることを確認できました

</details>

## まとめ

<details>
<summary>詳細</summary>

- SupabaseのDockerコンテナを利用したローカル環境の初歩的な部分について複数台のPCを使った環境でおおよそ問題なく使えることがわかりました。
- 実際にアプリケーションの開発を進めていくうえで必要となるSupabase本番とのプロジェクトのリンクやマイグレーション周りの動作確認は行えていません。。

この記事の中で紹介しているSupabase側と開発したアプリケーションのコードはそれぞれ以下のリポジトリで公開しています。

https://github.com/imo-tikuwa/nuxt-supabase-example-app-backend

https://github.com/imo-tikuwa/nuxt-supabase-example-app-frontend

</details>

## 参考リンク
- [Supabase CLI | Supabase Docs](https://supabase.com/docs/guides/cli/getting-started?queryGroups=platform&platform=windows)
  - Supabase CLIに関するドキュメント
- [supabase/cli: Supabase CLI. Manage postgres migrations, run Supabase locally, deploy edge functions. Postgres backups. Generating types from your database schema.](https://github.com/supabase/cli)
  - Supabase CLIのリポジトリ(GitHub)
  - ↑のドキュメントとどちらかをざっくり読んでおくのが良いかも
- [Windows&nbsp;11のIPアドレス（IPv4）を固定に設定する手順｜Aterm Q&amp;A｜目的別で探す｜Aterm（エーターム）&nbsp;サポートデスク](https://www.aterm.jp/support/qa/qa_external/00244/win11.html)
  - IPアドレスの固定化をする際に見たページ