<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: kanta13jp1</title>
    <description>The latest articles on Forem by kanta13jp1 (@kanta13jp1).</description>
    <link>https://forem.com/kanta13jp1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F801579%2F93001d32-b560-4f80-9b6e-732e7ee424d2.jpg</url>
      <title>Forem: kanta13jp1</title>
      <link>https://forem.com/kanta13jp1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kanta13jp1"/>
    <language>en</language>
    <item>
      <title>How a Bash Quoting Artifact Broke Our Production PostgreSQL Deploy</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Mon, 13 Apr 2026 09:43:44 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/how-a-bash-quoting-artifact-broke-our-production-postgresql-deploy-3n66</link>
      <guid>https://forem.com/kanta13jp1/how-a-bash-quoting-artifact-broke-our-production-postgresql-deploy-3n66</guid>
      <description>&lt;h1&gt;
  
  
  How a Bash Quoting Artifact Broke Our Production PostgreSQL Deploy
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Mystery Error
&lt;/h2&gt;

&lt;p&gt;Our production Supabase migration deploy suddenly started failing with &lt;code&gt;SQLSTATE 42601&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;syntax&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;at&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;near&lt;/span&gt; &lt;span class="nv"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="nv"&gt; (SQLSTATE 42601)
At statement: 0
-- AI University content: Hailuo AI (MiniMax)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SQL file looked perfectly fine in the editor. After investigation, the culprit turned out to be a &lt;strong&gt;bash shell quoting artifact&lt;/strong&gt; (&lt;code&gt;'"'"'&lt;/code&gt;) that had leaked directly into a SQL migration file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is &lt;code&gt;'"'"'&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;In bash, you can't use a single quote &lt;code&gt;'&lt;/code&gt; inside a single-quoted string. The workaround is to close the string, insert the character in double-quotes, then reopen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Three-part construction:&lt;/span&gt;
&lt;span class="s1"&gt;'...'&lt;/span&gt;   &lt;span class="c"&gt;# single-quoted string&lt;/span&gt;
&lt;span class="s2"&gt;"'"&lt;/span&gt;     &lt;span class="c"&gt;# single quote in double-quotes&lt;/span&gt;
&lt;span class="s1"&gt;'...'&lt;/span&gt;   &lt;span class="c"&gt;# resume single-quoted string&lt;/span&gt;

&lt;span class="c"&gt;# Commonly seen in curl -d:&lt;/span&gt;
curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt;&lt;span class="s2"&gt;"'{"&lt;/span&gt;key&lt;span class="s2"&gt;":"&lt;/span&gt;value&lt;span class="s2"&gt;"}'"&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt;
&lt;span class="c"&gt;# Expands to: '{"key":"value"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;'"'"'&lt;/code&gt; pattern is a valid bash escaping technique — but it's meaningless (and dangerous) in SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Went Wrong
&lt;/h2&gt;

&lt;p&gt;We were inserting API documentation with curl examples directly into a SQL migration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Problematic SQL (shell quoting artifact leaked in)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'hailuo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'API Guide'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'...curl examples...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;  -d '&lt;/span&gt;&lt;span class="nv"&gt;"'"&lt;/span&gt;&lt;span class="s1"&gt;'{&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;    "model": "video-01"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;  }'&lt;/span&gt;&lt;span class="nv"&gt;"'"&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The bash curl example was copy-pasted into SQL string content without being escaped.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what PostgreSQL sees when it hits &lt;code&gt;'"'"'&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;'&lt;/code&gt; → string start&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"&lt;/code&gt; → literal &lt;code&gt;"&lt;/code&gt; character&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'&lt;/code&gt; → &lt;strong&gt;string end&lt;/strong&gt; (parser thinks the string is closed here)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"'&lt;/code&gt; → &lt;strong&gt;syntax error 42601&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: SQL escape with &lt;code&gt;''&lt;/code&gt; (what we used)
&lt;/h3&gt;

&lt;p&gt;In PostgreSQL E-string notation (&lt;code&gt;E'...'&lt;/code&gt;), escape single quotes by doubling them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fixed&lt;/span&gt;
&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'...-d &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;{&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;n    "model": "video-01"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;n  }&lt;/span&gt;&lt;span class="se"&gt;''\\&lt;/span&gt;&lt;span class="s1"&gt;n...'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python batch fix across migration files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;migration.sql&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"''"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;migration.sql&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 2: Dollar-quoting (cleaner for long content)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- No escaping needed inside $$...$$&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "video-01"}'&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For long content with many single quotes, dollar-quoting is much more readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. CI Gate — catch it before it reaches production
&lt;/h3&gt;

&lt;p&gt;We added this step to our &lt;code&gt;ci.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check SQL migration shell quoting artifacts&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;python3 -c "&lt;/span&gt;
    &lt;span class="s"&gt;import os, sys&lt;/span&gt;
    &lt;span class="s"&gt;found = []&lt;/span&gt;
    &lt;span class="s"&gt;for f in os.listdir('supabase/migrations'):&lt;/span&gt;
        &lt;span class="s"&gt;if f.endswith('.sql'):&lt;/span&gt;
            &lt;span class="s"&gt;path = os.path.join('supabase/migrations', f)&lt;/span&gt;
            &lt;span class="s"&gt;content = open(path, errors='replace').read()&lt;/span&gt;
            &lt;span class="s"&gt;if \"'\\\"'\\\"'\" in content:&lt;/span&gt;
                &lt;span class="s"&gt;found.append(path)&lt;/span&gt;
    &lt;span class="s"&gt;if found:&lt;/span&gt;
        &lt;span class="s"&gt;print('FOUND: ' + ', '.join(found))&lt;/span&gt;
        &lt;span class="s"&gt;sys.exit(1)&lt;/span&gt;
    &lt;span class="s"&gt;else:&lt;/span&gt;
        &lt;span class="s"&gt;print(f'OK: {len(found)} issues in migrations')&lt;/span&gt;
    &lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This blocks the deploy before the artifact reaches Supabase.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use dollar-quoting for shell code samples
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Safe: no quoting issues&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="n"&gt;curl&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="n"&gt;POST&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="nv"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "video-01"}'&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Also fixed: GitHub Actions workflow quoting
&lt;/h3&gt;

&lt;p&gt;A related issue: &lt;code&gt;${{ steps.outputs.title }}&lt;/code&gt; injected directly into a bash string breaks when the title contains &lt;code&gt;"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Dangerous — breaks if title has double-quotes&lt;/span&gt;
&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;TITLE="${{ steps.meta.outputs.title }}"&lt;/span&gt;

&lt;span class="c1"&gt;# Safe — pass through env: block&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ARTICLE_TITLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.title }}&lt;/span&gt;
&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;TITLE="$ARTICLE_TITLE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions substitutes &lt;code&gt;${{ }}&lt;/code&gt; expressions &lt;em&gt;before&lt;/em&gt; the shell runs — so any &lt;code&gt;"&lt;/code&gt; in the value terminates the outer string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Root Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SQLSTATE 42601&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bash &lt;code&gt;'"'"'&lt;/code&gt; artifact in SQL string&lt;/td&gt;
&lt;td&gt;Replace with &lt;code&gt;''&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions syntax error&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;${{ expr }}&lt;/code&gt; with &lt;code&gt;"&lt;/code&gt; in value&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;env:&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recurring deploy failures&lt;/td&gt;
&lt;td&gt;No pre-deploy SQL lint&lt;/td&gt;
&lt;td&gt;Added CI check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway&lt;/strong&gt;: When embedding shell code samples in SQL migration files, always use dollar-quoting (&lt;code&gt;$$&lt;/code&gt;) or sanitize &lt;code&gt;'"'"'&lt;/code&gt; → &lt;code&gt;''&lt;/code&gt; before committing. Add a CI gate to catch it automatically.&lt;/p&gt;




&lt;p&gt;Building in public: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  PostgreSQL #Supabase #bash #buildinpublic #GitHubActions
&lt;/h1&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>bash</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Bash の '"'"' がSQLファイルに混入して本番デプロイを破壊した話</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Mon, 13 Apr 2026 09:19:21 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/bash-no-gasqlhuairunihun-ru-siteben-fan-depuroiwopo-huai-sitahua-4mbm</link>
      <guid>https://forem.com/kanta13jp1/bash-no-gasqlhuairunihun-ru-siteben-fan-depuroiwopo-huai-sitahua-4mbm</guid>
      <description>&lt;h1&gt;
  
  
  Bash の &lt;code&gt;'"'"'&lt;/code&gt; が SQL ファイルに混入して本番デプロイを破壊した話
&lt;/h1&gt;

&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;自分株式会社の本番 Supabase migration デプロイが突然 &lt;code&gt;SQLSTATE 42601&lt;/code&gt; で失敗し始めました。&lt;br&gt;
エラーメッセージは謎めいており、原因特定に時間がかかりました。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: syntax error at or near "'"" (SQLSTATE 42601)
At statement: 0
-- Windows版#61: AI大学43社目 — Hailuo AI (MiniMax)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;同一ファイルを目視で確認しても問題がわからない…。最終的に判明した原因は &lt;strong&gt;bash シェルのクォーティングパターン &lt;code&gt;'"'"'&lt;/code&gt; が SQL ファイルにそのまま混入していた&lt;/strong&gt;ことでした。&lt;/p&gt;

&lt;h2&gt;
  
  
  bash の &lt;code&gt;'"'"'&lt;/code&gt; とは何か
&lt;/h2&gt;

&lt;p&gt;bash でシングルクォート文字列の中にシングルクォート &lt;code&gt;'&lt;/code&gt; を入れるには、一度クォートを閉じて文字を挟んで再開する必要があります：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# bash での書き方: 3部構成&lt;/span&gt;
&lt;span class="s1"&gt;'...'&lt;/span&gt;   &lt;span class="c"&gt;# シングルクォート文字列&lt;/span&gt;
&lt;span class="s2"&gt;"'"&lt;/span&gt;     &lt;span class="c"&gt;# ダブルクォート内のシングルクォート (= ' 文字)&lt;/span&gt;
&lt;span class="s1"&gt;'...'&lt;/span&gt;   &lt;span class="c"&gt;# 再開&lt;/span&gt;

&lt;span class="c"&gt;# これを連結すると:&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"key": "value"}'&lt;/span&gt;  &lt;span class="c"&gt;# → {"key": "value"}&lt;/span&gt;

&lt;span class="c"&gt;# curl の -d オプションでよく使われるパターン:&lt;/span&gt;
curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt;&lt;span class="s2"&gt;"'{"&lt;/span&gt;key&lt;span class="s2"&gt;":"&lt;/span&gt;value&lt;span class="s2"&gt;"}'"&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt;
&lt;span class="c"&gt;# これは: ' + {"key":"value"} + ' と展開され&lt;/span&gt;
&lt;span class="c"&gt;# → '{"key":"value"}' となる&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;この &lt;code&gt;'"'"'&lt;/code&gt; というパターンは bash スクリプトのエスケープ技法です。&lt;/p&gt;

&lt;h2&gt;
  
  
  何が問題だったのか
&lt;/h2&gt;

&lt;p&gt;AI大学コンテンツの migration ファイルに、curl の使用例を含む API ガイドが入っていました：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 問題のある SQL (shell quoting artifact が混入)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'hailuo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'API Guide'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'...curl examples...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;  -d '&lt;/span&gt;&lt;span class="nv"&gt;"'"&lt;/span&gt;&lt;span class="s1"&gt;'{&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;    "model": "video-01"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;  }'&lt;/span&gt;&lt;span class="nv"&gt;"'"&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;markdown や Shell で書かれたコンテンツをそのまま SQL の文字列値に埋め込む際に、bash quoting がそのままコピーされてしまった&lt;/strong&gt;のです。&lt;/p&gt;

&lt;p&gt;PostgreSQL に &lt;code&gt;'"'"'&lt;/code&gt; を渡すと：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;'&lt;/code&gt; → 文字列開始&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"&lt;/code&gt; → &lt;code&gt;"&lt;/code&gt; 文字&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'&lt;/code&gt; → 文字列終了！（ここで SQL パーサーは文字列が終わったと解釈）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"'&lt;/code&gt; → &lt;strong&gt;構文エラー&lt;/strong&gt; &lt;code&gt;42601&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  修正方法
&lt;/h2&gt;

&lt;h3&gt;
  
  
  オプション1: &lt;code&gt;''&lt;/code&gt; でエスケープ (採用)
&lt;/h3&gt;

&lt;p&gt;SQL の E-string 記法 (&lt;code&gt;E'...'&lt;/code&gt;) では、シングルクォートは &lt;code&gt;''&lt;/code&gt;（2つ連続）でエスケープします：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 修正後&lt;/span&gt;
&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'...-d &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;{&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;n    "model": "video-01"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;n  }&lt;/span&gt;&lt;span class="se"&gt;''\\&lt;/span&gt;&lt;span class="s1"&gt;n...'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python でバッチ修正：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;migration.sql&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# '"'"' を '' に置換
&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"''"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;migration.sql&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  オプション2: ドル引用符を使う
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ドル引用符ならシングルクォートをエスケープ不要&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "video-01"}'&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;大量のシングルクォートを含む長文コンテンツには、ドル引用符の方が読みやすいです。&lt;/p&gt;

&lt;h2&gt;
  
  
  再発防止策
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Supabase migration ファイルを生成するときの注意
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Shell スクリプトのコードサンプルは避けるか、事前変換する&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;'"'"'&lt;/code&gt; → &lt;code&gt;''&lt;/code&gt; に変換してから埋め込む&lt;/li&gt;
&lt;li&gt;または &lt;code&gt;$$...$$&lt;/code&gt; ドル引用符を使う&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CI で事前 lint する&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;grep -n "'\"'\"'" supabase/migrations/*.sql&lt;/code&gt; で artifact を検出&lt;/li&gt;
&lt;li&gt;PR チェックに追加する&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ローカルで dry-run 実行&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   supabase db push &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;現象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;修正&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SQLSTATE 42601&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bash &lt;code&gt;'"'"'&lt;/code&gt; がSQL文字列に混入&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;''&lt;/code&gt; に置換&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deploy-prod 連続失敗&lt;/td&gt;
&lt;td&gt;migration ファイルに構文エラー&lt;/td&gt;
&lt;td&gt;対象ファイルを修正してプッシュ&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;教訓&lt;/strong&gt;: SQL migration ファイルに Shell スクリプトのコードサンプルを埋め込む場合は、bash クォーティング変換が不要になるドル引用符 (&lt;code&gt;$$&lt;/code&gt;) を使うか、&lt;code&gt;'"'"'&lt;/code&gt; → &lt;code&gt;''&lt;/code&gt; を確認してから push する。&lt;/p&gt;




&lt;p&gt;自分株式会社: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  PostgreSQL #Supabase #bash #buildinpublic #個人開発
&lt;/h1&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>githubactions</category>
      <category>bash</category>
    </item>
    <item>
      <title>Adding Persistent Memory to Claude Code with claude-mem — Plus a DIY Lightweight Alternative</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Mon, 13 Apr 2026 09:11:53 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/adding-persistent-memory-to-claude-code-with-claude-mem-plus-a-diy-lightweight-alternative-4gha</link>
      <guid>https://forem.com/kanta13jp1/adding-persistent-memory-to-claude-code-with-claude-mem-plus-a-diy-lightweight-alternative-4gha</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Claude Code Forgets Everything
&lt;/h2&gt;

&lt;p&gt;Every time you start a new Claude Code session, the slate is wiped clean. Your coding style preferences, project architecture decisions, yesterday's debugging session — all gone.&lt;/p&gt;

&lt;p&gt;You end up repeating yourself: "We use Supabase, not Firebase. The Edge Functions are in &lt;code&gt;supabase/functions/&lt;/code&gt;. Don't use dummy data."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;claude-mem&lt;/strong&gt; fixes this by adding persistent memory across sessions. It hit 46K GitHub stars within 48 hours of launch. I installed it, built a lightweight DIY alternative first, and here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is claude-mem?
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/thedotmack/claude-mem" rel="noopener noreferrer"&gt;https://github.com/thedotmack/claude-mem&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A plugin that gives Claude Code a long-term memory. It automatically captures what you do during sessions and injects relevant context into future conversations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5 Lifecycle Hooks&lt;/strong&gt;: SessionStart / UserPromptSubmit / PostToolUse / Stop / SessionEnd&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite + Chroma&lt;/strong&gt;: Hybrid search (keyword + vector similarity)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bun HTTP Worker&lt;/strong&gt;: Background service on localhost:37777&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Tools&lt;/strong&gt;: 3-layer progressive disclosure (search → timeline → get_observations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web UI&lt;/strong&gt;: Visual memory browser&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx claude-mem &lt;span class="nb"&gt;install
&lt;/span&gt;npx claude-mem start  &lt;span class="c"&gt;# Requires Bun&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The DIY Alternative I Built First
&lt;/h2&gt;

&lt;p&gt;Before discovering claude-mem, I built a minimal memory system using just two PowerShell scripts and Claude Code's native hooks API.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostToolUse Hook (auto-capture.ps1)
&lt;/h3&gt;

&lt;p&gt;Triggered after every &lt;code&gt;Bash&lt;/code&gt; or &lt;code&gt;Write&lt;/code&gt; tool use. Captures git commits and new file creations to a daily markdown file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;memory/auto-capture/2026-04-13.md
&lt;span class="p"&gt;-&lt;/span&gt; 09:15 [abc1234] feat: Add user authentication
&lt;span class="p"&gt;-&lt;/span&gt; 09:32 [Write] auth_middleware.dart
&lt;span class="p"&gt;-&lt;/span&gt; 10:01 [def5678] fix: Token refresh logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SessionStart Hook (session-resume.ps1)
&lt;/h3&gt;

&lt;p&gt;Reads the last 3 days of captures and injects them as context when a new session starts. The AI immediately knows what you've been working on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registration in settings.json
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"powershell -File auto-capture.ps1"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SessionStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"powershell -File session-resume.ps1"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Head-to-Head Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;claude-mem&lt;/th&gt;
&lt;th&gt;DIY Hooks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npx install&lt;/code&gt; (1 command)&lt;/td&gt;
&lt;td&gt;2 scripts, manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-capture&lt;/td&gt;
&lt;td&gt;All tool usage&lt;/td&gt;
&lt;td&gt;git commits + Write only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;Vector similarity + keyword&lt;/td&gt;
&lt;td&gt;grep (text search)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;td&gt;localhost:37777&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;Bun + SQLite + (Chroma)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token cost&lt;/td&gt;
&lt;td&gt;LLM compression (Gemini = free)&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git-friendly&lt;/td&gt;
&lt;td&gt;DB file (gitignored)&lt;/td&gt;
&lt;td&gt;Markdown files (shareable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-instance&lt;/td&gt;
&lt;td&gt;Session-scoped isolation&lt;/td&gt;
&lt;td&gt;File sharing for coordination&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Running Both Together
&lt;/h2&gt;

&lt;p&gt;The good news: &lt;strong&gt;they coexist perfectly&lt;/strong&gt;. claude-mem registers as a plugin, DIY hooks register directly in settings.json. Both fire on the same events without conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  When claude-mem shines
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart compression&lt;/strong&gt;: Uses an LLM (Gemini/Claude) to summarize tool outputs into compact observations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic search&lt;/strong&gt;: "What did I do with the auth system last week?" actually works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web dashboard&lt;/strong&gt;: Visual overview of what's been captured&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When DIY hooks shine
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt;: No server, no database, no runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team sharing&lt;/strong&gt;: Markdown files can be committed to git and shared across instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full control&lt;/strong&gt;: You decide exactly what gets captured and how&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Truly free&lt;/strong&gt;: No API calls whatsoever&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost Optimization Tip
&lt;/h2&gt;

&lt;p&gt;claude-mem defaults to using Claude API for compression, which consumes your tokens. Switch to Gemini (free) to eliminate this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;~/.claude-mem/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CLAUDE_MEM_PROVIDER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gemini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CLAUDE_MEM_GEMINI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-free-key-from-aistudio.google.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Our 3-Layer Memory Architecture
&lt;/h2&gt;

&lt;p&gt;In our project (Flutter Web + Supabase, 3 parallel Claude Code instances), we use a layered approach:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1: Intra-session&lt;/td&gt;
&lt;td&gt;claude-mem (SQLite)&lt;/td&gt;
&lt;td&gt;Auto-record all tool usage, semantic search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2: Inter-session&lt;/td&gt;
&lt;td&gt;DIY hooks (markdown)&lt;/td&gt;
&lt;td&gt;Git commit history, cross-instance sharing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3: Cross-project&lt;/td&gt;
&lt;td&gt;NotebookLM Master Brain&lt;/td&gt;
&lt;td&gt;Deep research, long-term architectural knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;claude-mem delivers on its promise of turning Claude Code from a "disposable tool" into a "growing partner." The vector search and Web UI are genuinely useful features that are hard to replicate with simple scripts.&lt;/p&gt;

&lt;p&gt;However, for teams that want zero dependencies, zero token cost, and git-friendly memory sharing, a DIY hook approach is a solid starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My recommendation&lt;/strong&gt;: Start with DIY hooks for minimal memory, then layer on claude-mem when you need semantic search and automatic compression.&lt;/p&gt;




&lt;p&gt;Built with &lt;a href="https://claude.ai/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; | Project: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ClaudeCode #AI #buildinpublic
&lt;/h1&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>llm</category>
    </item>
    <item>
      <title>Claude Codeに永続メモリを追加する「claude-mem」を実際に導入してみた — 自作hook版との比較</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Mon, 13 Apr 2026 09:08:48 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/claude-codeniyong-sok-memoriwozhui-jia-suruclaude-mem-woshi-ji-nidao-ru-sitemita-zi-zuo-hookban-tonobi-jiao-124m</link>
      <guid>https://forem.com/kanta13jp1/claude-codeniyong-sok-memoriwozhui-jia-suruclaude-mem-woshi-ji-nidao-ru-sitemita-zi-zuo-hookban-tonobi-jiao-124m</guid>
      <description>&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;Claude Codeを日常的に使っていて、こんな経験はないだろうか。&lt;/p&gt;

&lt;p&gt;「昨日の続きをやって」と言ったのに、まったく覚えていない。&lt;br&gt;
毎回プロジェクトの構成を説明し直す。前回のセッションで学んだ教訓がリセットされる。&lt;/p&gt;

&lt;p&gt;これを解決するツール &lt;strong&gt;claude-mem&lt;/strong&gt; が公開48時間で4.6万スターを突破した。実際に導入してみたので、自作の軽量版hookとの比較も含めてレポートする。&lt;/p&gt;
&lt;h2&gt;
  
  
  claude-mem とは
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/thedotmack/claude-mem" rel="noopener noreferrer"&gt;https://github.com/thedotmack/claude-mem&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Codeに「永続メモリ」を追加するプラグイン。セッション間で文脈を引き継ぎ、過去の作業内容・コーディングスタイル・プロジェクト知識を蓄積する。&lt;/p&gt;
&lt;h3&gt;
  
  
  技術アーキテクチャ
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5つのLifecycle Hooks&lt;/strong&gt;: SessionStart / UserPromptSubmit / PostToolUse / Stop / SessionEnd&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite + Chroma&lt;/strong&gt;: ハイブリッド検索（キーワード + ベクター類似度）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bun HTTP Worker&lt;/strong&gt;: localhost:37777 で常駐サービス&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Tools&lt;/strong&gt;: search / timeline / get_observations の3層検索&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web UI&lt;/strong&gt;: ブラウザで蓄積状況を可視化&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  インストール
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx claude-mem &lt;span class="nb"&gt;install
&lt;/span&gt;npx claude-mem start  &lt;span class="c"&gt;# Worker起動（Bun必須）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  導入前に自作した軽量版
&lt;/h2&gt;

&lt;p&gt;実はclaude-memを知る前に、同じ課題を解決する軽量hookを自作していた。&lt;/p&gt;
&lt;h3&gt;
  
  
  自作版の設計
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostToolUse hook (auto-capture.ps1)
  └─ git commit / Write → memory/auto-capture/YYYY-MM-DD.md に追記

SessionStart hook (session-resume.ps1)
  └─ 直近3日分のキャプチャを読み込み → コンテキスト注入
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;特徴&lt;/strong&gt;: 外部サーバー不要。ファイルベース。PowerShellスクリプト2本だけ。&lt;/p&gt;
&lt;h3&gt;
  
  
  settings.json への登録
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"powershell -File auto-capture.ps1"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SessionStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"powershell -File session-resume.ps1"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  claude-mem vs 自作hook 比較
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;機能&lt;/th&gt;
&lt;th&gt;claude-mem&lt;/th&gt;
&lt;th&gt;自作hook&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;セットアップ&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npx install&lt;/code&gt; 1コマンド&lt;/td&gt;
&lt;td&gt;スクリプト2本手書き&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自動キャプチャ&lt;/td&gt;
&lt;td&gt;全ツール使用を記録&lt;/td&gt;
&lt;td&gt;git commit + Write のみ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;検索&lt;/td&gt;
&lt;td&gt;ベクター類似度 + キーワード&lt;/td&gt;
&lt;td&gt;grep (テキスト検索)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;td&gt;localhost:37777 で可視化&lt;/td&gt;
&lt;td&gt;なし&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;外部依存&lt;/td&gt;
&lt;td&gt;Bun + SQLite + (Chroma)&lt;/td&gt;
&lt;td&gt;なし&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;トークン消費&lt;/td&gt;
&lt;td&gt;圧縮にLLM使用 (Gemini無料可)&lt;/td&gt;
&lt;td&gt;ゼロ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git管理&lt;/td&gt;
&lt;td&gt;DBファイル (gitignore)&lt;/td&gt;
&lt;td&gt;mdファイル (git共有可)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;複数インスタンス&lt;/td&gt;
&lt;td&gt;セッション単位で分離&lt;/td&gt;
&lt;td&gt;ファイル共有で協調&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  実際に両方を併用してみた結果
&lt;/h2&gt;

&lt;p&gt;結論: &lt;strong&gt;共存できる&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;claude-memはプラグインとしてhooksを登録し、自作hookはsettings.jsonに直接登録。両方が同時に動作する。&lt;/p&gt;
&lt;h3&gt;
  
  
  claude-memの良い点
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;圧縮が賢い&lt;/strong&gt;: LLM（Gemini等）で要約するので、大量のツール使用をコンパクトに記憶&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;検索が強力&lt;/strong&gt;: 「先週のAPI実装」のような自然言語クエリで過去の作業を検索可能&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web UIが便利&lt;/strong&gt;: 何が記憶されているか一目で確認できる&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  自作hookの良い点
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ゼロ依存&lt;/strong&gt;: サーバー起動不要。スクリプトだけで完結&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;git共有&lt;/strong&gt;: チーム（複数インスタンス）で記憶を共有できる&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;完全無料&lt;/strong&gt;: LLM APIを一切使わない&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;カスタマイズ自在&lt;/strong&gt;: 何をキャプチャするか完全に制御可能&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  コスト最適化のコツ
&lt;/h2&gt;

&lt;p&gt;claude-memはデフォルトでClaude APIを使って圧縮するため、トークンを消費する。以下の設定で無料化できる:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;~/.claude-mem/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CLAUDE_MEM_PROVIDER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gemini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CLAUDE_MEM_GEMINI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-key-here"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google AI StudioでGemini API Keyを無料取得し、圧縮処理をGeminiに委譲。Claude側のトークン消費はゼロになる。&lt;/p&gt;

&lt;h2&gt;
  
  
  我々のプロジェクトでの活用
&lt;/h2&gt;

&lt;p&gt;自分株式会社（Flutter Web + Supabase）では3インスタンス並行開発を行っている。各インスタンスの知識共有に以下の3層メモリを使い分けている:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;層&lt;/th&gt;
&lt;th&gt;ツール&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1: セッション内&lt;/td&gt;
&lt;td&gt;claude-mem (SQLite)&lt;/td&gt;
&lt;td&gt;ツール使用の自動記録・検索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2: セッション間&lt;/td&gt;
&lt;td&gt;自作hook (mdファイル)&lt;/td&gt;
&lt;td&gt;git commit履歴・インスタンス間共有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3: プロジェクト横断&lt;/td&gt;
&lt;td&gt;NotebookLM Master Brain&lt;/td&gt;
&lt;td&gt;深い調査・長期知識の蓄積&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;p&gt;claude-memは「Claude Codeを育てるパートナーに変える」ツールとして、宣伝文句に偽りなし。特にベクター検索とWeb UIは自作では難しい。&lt;/p&gt;

&lt;p&gt;ただし、シンプルなユースケースなら自作hookで十分。外部依存なし・トークン消費なし・git共有可能という利点がある。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;おすすめ&lt;/strong&gt;: まず自作hookで最小限のメモリを実装し、必要に応じてclaude-memを追加導入する「段階的アプローチ」が最も合理的。&lt;/p&gt;




&lt;p&gt;URL: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/thedotmack/claude-mem" rel="noopener noreferrer"&gt;https://github.com/thedotmack/claude-mem&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ClaudeCode #AI #buildinpublic
&lt;/h1&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>claudemem</category>
    </item>
    <item>
      <title>AI大学40社体制完成 + Supabase ON CONFLICT本番デプロイ障害を修正した話</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Mon, 13 Apr 2026 07:00:11 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/aida-xue-40she-ti-zhi-wan-cheng-supabase-on-conflictben-fan-depuroizhang-hai-woxiu-zheng-sitahua-3ddh</link>
      <guid>https://forem.com/kanta13jp1/aida-xue-40she-ti-zhi-wan-cheng-supabase-on-conflictben-fan-depuroizhang-hai-woxiu-zheng-sitahua-3ddh</guid>
      <description>&lt;h1&gt;
  
  
  AI大学40社体制完成 + Supabase ON CONFLICT本番デプロイ障害を修正した話
&lt;/h1&gt;

&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;Flutter Web × Supabase で開発中の自分株式会社で、本日 AI大学機能が &lt;strong&gt;40社体制&lt;/strong&gt; に到達しました。&lt;/p&gt;

&lt;p&gt;同時に本番デプロイが &lt;code&gt;SQLSTATE 42P10&lt;/code&gt; エラーで失敗するという障害が発生。原因調査と修正を即日対応したので、その記録を共有します。&lt;/p&gt;

&lt;h2&gt;
  
  
  AI大学とは
&lt;/h2&gt;

&lt;p&gt;自分株式会社の「AI大学」機能は、主要 AI プロバイダー（Google/OpenAI/Anthropic など）について以下を学べる機能です：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;各社の概要・モデル一覧・API ガイド&lt;/li&gt;
&lt;li&gt;クイズで知識確認&lt;/li&gt;
&lt;li&gt;学習スコア・ストリーク記録&lt;/li&gt;
&lt;li&gt;ランキングで競争&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本日で登録プロバイダーが 40社になりました：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;google, openai, anthropic, microsoft, meta, x, deepseek, mistral, perplexity,
groq, cohere, amazon, stability, huggingface, nvidia, ibm, sakana, baidu,
oracle, reka, aleph_alpha, together_ai, fireworks_ai, replicate, writer,
ai21, voyage, elevenlabs, openrouter, ollama, runway, suno, ideogram, udio,
luma, kling, pika, assemblyai, twelve_labs, cohere(重複除外後 39社+α)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  本番デプロイ障害: SQLSTATE 42P10
&lt;/h2&gt;

&lt;h3&gt;
  
  
  エラー内容
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;there&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;no&lt;/span&gt; &lt;span class="k"&gt;unique&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;exclusion&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;matching&lt;/span&gt;
       &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="n"&gt;specification&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SQLSTATE&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="n"&gt;P10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions の &lt;code&gt;deploy-prod.yml&lt;/code&gt; が Supabase migration 適用時に失敗。&lt;/p&gt;

&lt;h3&gt;
  
  
  原因
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ai_university_content&lt;/code&gt; テーブルの DDL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;category&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;-- ... 他カラム&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;ai_university_content_provider_idx&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;INDEX のみで UNIQUE 制約なし&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;一方、新しい migration（40社目前後から）は：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;  &lt;span class="c1"&gt;-- ← UNIQUE制約が必要!&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ON CONFLICT (provider, category)&lt;/code&gt; は &lt;strong&gt;UNIQUE 制約または EXCLUDE 制約&lt;/strong&gt; が必要です。&lt;br&gt;
INDEX だけでは PostgreSQL は使えません。&lt;/p&gt;
&lt;h3&gt;
  
  
  修正方法
&lt;/h3&gt;

&lt;p&gt;新しい migration ファイル &lt;code&gt;20260412029500_add_unique_constraint.sql&lt;/code&gt; を追加：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Remove duplicate rows first (keep most recently updated)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Add the UNIQUE constraint required for ON CONFLICT upsert&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ai_university_content_provider_category_unique&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ポイント&lt;/strong&gt;: migration は順序通りに適用されるため、このファイルに &lt;code&gt;20260412029500&lt;/code&gt; というタイムスタンプをつけて、問題の migration（&lt;code&gt;20260412030000&lt;/code&gt;〜）の直前に挿入しました。&lt;/p&gt;

&lt;h3&gt;
  
  
  重複行の削除について
&lt;/h3&gt;

&lt;p&gt;本番 DB に既存データがある場合、&lt;code&gt;UNIQUE&lt;/code&gt; 制約追加前に重複を除去する必要があります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="c1"&gt;-- idが小さい方（古い方）を削除&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;この書き方は PostgreSQL の &lt;code&gt;DELETE ... USING&lt;/code&gt; 構文で、自己結合して重複を削除できます。&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL の ON CONFLICT まとめ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;書き方&lt;/th&gt;
&lt;th&gt;必要な前提&lt;/th&gt;
&lt;th&gt;動作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不要&lt;/td&gt;
&lt;td&gt;競合時は無視&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ON CONFLICT (col) DO UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;col&lt;/code&gt; に UNIQUE/EXCLUDE 制約が必要&lt;/td&gt;
&lt;td&gt;競合時は UPDATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ON CONFLICT ON CONSTRAINT name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;制約名を指定&lt;/td&gt;
&lt;td&gt;制約に合致した競合時に UPDATE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;早期の migration では &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;（制約不要）を使っていましたが、後から upsert に変更した際に制約を追加し忘れたのが原因でした。&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase migration の注意点
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;migration は不可逆&lt;/strong&gt; — 一度 push した migration は修正ではなく新しい migration を追加して対処&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;本番 DB のデータを変更する migration は慎重に&lt;/strong&gt; — &lt;code&gt;DELETE&lt;/code&gt; を含む migration は特に&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ON CONFLICT&lt;/code&gt; を使う場合は制約を先に追加&lt;/strong&gt; — DDL migration → seed migration の順&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI大学が 40社体制に到達&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ON CONFLICT (provider, category)&lt;/code&gt; エラーの根本原因は UNIQUE 制約の欠如&lt;/li&gt;
&lt;li&gt;重複削除 + 制約追加 migration を挿入することで修正&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supabase db push --include-all&lt;/code&gt; は migration の順序が重要&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;自分株式会社: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  buildinpublic #Flutter #Supabase #PostgreSQL #個人開発
&lt;/h1&gt;

</description>
      <category>flutter</category>
      <category>supabase</category>
      <category>postgres</category>
    </item>
    <item>
      <title>AI大学40社体制完成 + Supabase ON CONFLICT本番デプロイ障害を修正した話</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Sun, 12 Apr 2026 12:09:45 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/aida-xue-40she-ti-zhi-wan-cheng-supabase-on-conflictben-fan-depuroizhang-hai-woxiu-zheng-sitahua-bm4</link>
      <guid>https://forem.com/kanta13jp1/aida-xue-40she-ti-zhi-wan-cheng-supabase-on-conflictben-fan-depuroizhang-hai-woxiu-zheng-sitahua-bm4</guid>
      <description>&lt;h1&gt;
  
  
  AI大学40社体制完成 + Supabase ON CONFLICT本番デプロイ障害を修正した話
&lt;/h1&gt;

&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;Flutter Web × Supabase で開発中の自分株式会社で、本日 AI大学機能が &lt;strong&gt;40社体制&lt;/strong&gt; に到達しました。&lt;/p&gt;

&lt;p&gt;同時に本番デプロイが &lt;code&gt;SQLSTATE 42P10&lt;/code&gt; エラーで失敗するという障害が発生。原因調査と修正を即日対応したので、その記録を共有します。&lt;/p&gt;

&lt;h2&gt;
  
  
  AI大学とは
&lt;/h2&gt;

&lt;p&gt;自分株式会社の「AI大学」機能は、主要 AI プロバイダー（Google/OpenAI/Anthropic など）について以下を学べる機能です：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;各社の概要・モデル一覧・API ガイド&lt;/li&gt;
&lt;li&gt;クイズで知識確認&lt;/li&gt;
&lt;li&gt;学習スコア・ストリーク記録&lt;/li&gt;
&lt;li&gt;ランキングで競争&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本日で登録プロバイダーが 40社になりました：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;google, openai, anthropic, microsoft, meta, x, deepseek, mistral, perplexity,
groq, cohere, amazon, stability, huggingface, nvidia, ibm, sakana, baidu,
oracle, reka, aleph_alpha, together_ai, fireworks_ai, replicate, writer,
ai21, voyage, elevenlabs, openrouter, ollama, runway, suno, ideogram, udio,
luma, kling, pika, assemblyai, twelve_labs, cohere(重複除外後 39社+α)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  本番デプロイ障害: SQLSTATE 42P10
&lt;/h2&gt;

&lt;h3&gt;
  
  
  エラー内容
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;there&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;no&lt;/span&gt; &lt;span class="k"&gt;unique&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;exclusion&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;matching&lt;/span&gt;
       &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="n"&gt;specification&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SQLSTATE&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="n"&gt;P10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions の &lt;code&gt;deploy-prod.yml&lt;/code&gt; が Supabase migration 適用時に失敗。&lt;/p&gt;

&lt;h3&gt;
  
  
  原因
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ai_university_content&lt;/code&gt; テーブルの DDL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;category&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;-- ... 他カラム&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;ai_university_content_provider_idx&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;INDEX のみで UNIQUE 制約なし&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;一方、新しい migration（40社目前後から）は：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;  &lt;span class="c1"&gt;-- ← UNIQUE制約が必要!&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ON CONFLICT (provider, category)&lt;/code&gt; は &lt;strong&gt;UNIQUE 制約または EXCLUDE 制約&lt;/strong&gt; が必要です。&lt;br&gt;
INDEX だけでは PostgreSQL は使えません。&lt;/p&gt;
&lt;h3&gt;
  
  
  修正方法
&lt;/h3&gt;

&lt;p&gt;新しい migration ファイル &lt;code&gt;20260412029500_add_unique_constraint.sql&lt;/code&gt; を追加：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Remove duplicate rows first (keep most recently updated)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Add the UNIQUE constraint required for ON CONFLICT upsert&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ai_university_content_provider_category_unique&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ポイント&lt;/strong&gt;: migration は順序通りに適用されるため、このファイルに &lt;code&gt;20260412029500&lt;/code&gt; というタイムスタンプをつけて、問題の migration（&lt;code&gt;20260412030000&lt;/code&gt;〜）の直前に挿入しました。&lt;/p&gt;

&lt;h3&gt;
  
  
  重複行の削除について
&lt;/h3&gt;

&lt;p&gt;本番 DB に既存データがある場合、&lt;code&gt;UNIQUE&lt;/code&gt; 制約追加前に重複を除去する必要があります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="c1"&gt;-- idが小さい方（古い方）を削除&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;この書き方は PostgreSQL の &lt;code&gt;DELETE ... USING&lt;/code&gt; 構文で、自己結合して重複を削除できます。&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL の ON CONFLICT まとめ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;書き方&lt;/th&gt;
&lt;th&gt;必要な前提&lt;/th&gt;
&lt;th&gt;動作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不要&lt;/td&gt;
&lt;td&gt;競合時は無視&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ON CONFLICT (col) DO UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;col&lt;/code&gt; に UNIQUE/EXCLUDE 制約が必要&lt;/td&gt;
&lt;td&gt;競合時は UPDATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ON CONFLICT ON CONSTRAINT name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;制約名を指定&lt;/td&gt;
&lt;td&gt;制約に合致した競合時に UPDATE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;早期の migration では &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;（制約不要）を使っていましたが、後から upsert に変更した際に制約を追加し忘れたのが原因でした。&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase migration の注意点
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;migration は不可逆&lt;/strong&gt; — 一度 push した migration は修正ではなく新しい migration を追加して対処&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;本番 DB のデータを変更する migration は慎重に&lt;/strong&gt; — &lt;code&gt;DELETE&lt;/code&gt; を含む migration は特に&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ON CONFLICT&lt;/code&gt; を使う場合は制約を先に追加&lt;/strong&gt; — DDL migration → seed migration の順&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI大学が 40社体制に到達&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ON CONFLICT (provider, category)&lt;/code&gt; エラーの根本原因は UNIQUE 制約の欠如&lt;/li&gt;
&lt;li&gt;重複削除 + 制約追加 migration を挿入することで修正&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supabase db push --include-all&lt;/code&gt; は migration の順序が重要&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;自分株式会社: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  buildinpublic #Flutter #Supabase #PostgreSQL #個人開発
&lt;/h1&gt;

</description>
      <category>flutter</category>
      <category>supabase</category>
      <category>postgres</category>
    </item>
    <item>
      <title>How I Automated CS, Bug Fixes, and Competitor Monitoring with Claude Code Schedule</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Sun, 12 Apr 2026 09:43:44 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/how-i-automated-cs-bug-fixes-and-competitor-monitoring-with-claude-code-schedule-4494</link>
      <guid>https://forem.com/kanta13jp1/how-i-automated-cs-bug-fixes-and-competitor-monitoring-with-claude-code-schedule-4494</guid>
      <description>&lt;h1&gt;
  
  
  How I Automated CS, Bug Fixes, and Competitor Monitoring with Claude Code Schedule — Zero Servers, Zero API Cost
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Title Options
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;How I Automated CS, Bug Fixes, and Competitor Monitoring with Claude Code Schedule&lt;/li&gt;
&lt;li&gt;9 Automated Tasks Running 24/7 Without a Server — Claude Code Schedule&lt;/li&gt;
&lt;li&gt;From Support Tickets to Blog Drafts: Full Automation with Claude Code Schedule&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Target
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[x] dev.to (English, technical)&lt;/li&gt;
&lt;li&gt;[ ] Hashnode&lt;/li&gt;
&lt;li&gt;[ ] Medium&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Draft
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;I run a personal SaaS called "Jibun Corp" (自分株式会社) — an AI-integrated life management platform built with Flutter Web + Supabase. As a solo developer, I was drowning in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support ticket responses&lt;/li&gt;
&lt;li&gt;Bug reports and fixes&lt;/li&gt;
&lt;li&gt;Competitor monitoring across 14 products&lt;/li&gt;
&lt;li&gt;Daily reports and metrics tracking&lt;/li&gt;
&lt;li&gt;Blog post writing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Solution: Claude Code Schedule
&lt;/h3&gt;

&lt;p&gt;Claude Code recently added a "Schedule" feature. It runs tasks on a cron-like schedule, entirely in Anthropic's cloud. No server, no PC required, no API costs beyond the Pro plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  My 9 Automated Tasks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;daily-report&lt;/td&gt;
&lt;td&gt;Daily 9 AM&lt;/td&gt;
&lt;td&gt;Fetches metrics → generates report → posts to X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cs-check&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;Reads tickets → replies via FAQ or fixes bugs → escalates complex ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;weekly-sns-draft&lt;/td&gt;
&lt;td&gt;Weekly Mon&lt;/td&gt;
&lt;td&gt;Summarizes week → drafts social posts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;daily-development&lt;/td&gt;
&lt;td&gt;Daily 10 AM&lt;/td&gt;
&lt;td&gt;Pushes roadmap tasks forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pr-auto-review&lt;/td&gt;
&lt;td&gt;Every 3h&lt;/td&gt;
&lt;td&gt;Reviews open PRs for security, performance, logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;competitor-monitoring&lt;/td&gt;
&lt;td&gt;Daily 7 AM&lt;/td&gt;
&lt;td&gt;Checks 14 competitors' websites and news&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;infra-health-check&lt;/td&gt;
&lt;td&gt;Every 30 min&lt;/td&gt;
&lt;td&gt;Verifies DB connectivity and table availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dependency-audit&lt;/td&gt;
&lt;td&gt;Weekly Mon&lt;/td&gt;
&lt;td&gt;Checks for vulnerable packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blog-draft&lt;/td&gt;
&lt;td&gt;Daily 8 AM&lt;/td&gt;
&lt;td&gt;Generates technical blog drafts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLAUDE.md (task definitions)
    ↓
Claude Code Schedule (cron trigger)
    ↓
WebFetch → Supabase Edge Functions (thin API layer)
    ↓
Supabase PostgreSQL (data persistence)
    ↓
GitHub push → Firebase auto-deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Learnings
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edge Functions as thin APIs&lt;/strong&gt;: Schedule can only use WebFetch (HTTP), so create minimal Edge Functions that expose your data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;RLS matters&lt;/strong&gt;: Schedule runs as &lt;code&gt;service_role&lt;/code&gt;, so set up RLS policies accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log everything&lt;/strong&gt;: Created a &lt;code&gt;schedule_task_runs&lt;/code&gt; table to track execution status, viewable from the admin dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Claude Pro plan: $20/month (already paying for development)&lt;/li&gt;
&lt;li&gt;Additional API cost: $0&lt;/li&gt;
&lt;li&gt;Server cost: $0&lt;/li&gt;
&lt;li&gt;Total automation cost: $0&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Claude Code Schedule turns CLAUDE.md into a living operations manual. Write what you want done, set a schedule, and walk away.&lt;/p&gt;




&lt;p&gt;URL: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/kanta13jp1/my_web_app" rel="noopener noreferrer"&gt;https://github.com/kanta13jp1/my_web_app&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  flutter #supabase #claudecode #automation #buildinpublic
&lt;/h1&gt;

</description>
      <category>claudecode</category>
      <category>supabase</category>
      <category>githubactions</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Flutter Supabaseで「AI大学」を34社対応に拡張した話 — 毎日自動更新するAI学習プラットフォーム</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Sun, 12 Apr 2026 09:42:36 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/flutterxsupabasedeaida-xue-wo34she-dui-ying-nikuo-zhang-sitahua-mei-ri-zi-dong-geng-xin-suruaixue-xi-puratutohuomu-3nik</link>
      <guid>https://forem.com/kanta13jp1/flutterxsupabasedeaida-xue-wo34she-dui-ying-nikuo-zhang-sitahua-mei-ri-zi-dong-geng-xin-suruaixue-xi-puratutohuomu-3nik</guid>
      <description>&lt;h1&gt;
  
  
  Flutter×Supabaseで「AI大学」を34社対応に拡張した話 — 毎日自動更新するAI学習プラットフォーム
&lt;/h1&gt;

&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;個人開発アプリ「自分株式会社」の中で最も差別化が進んでいる機能が &lt;strong&gt;AI大学&lt;/strong&gt; です。&lt;/p&gt;

&lt;p&gt;Google、OpenAI、Anthropic、Meta、DeepSeekなど、乱立するAIプロバイダーを横断的に学習できるプラットフォームを Flutter Web + Supabase で構築し、&lt;strong&gt;34社対応&lt;/strong&gt;まで拡張しました。&lt;/p&gt;

&lt;p&gt;この記事では:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI大学の基本設計とDB構造&lt;/li&gt;
&lt;li&gt;34社のコンテンツを &lt;strong&gt;GitHub Actions で2時間ごと自動更新&lt;/strong&gt; する仕組み&lt;/li&gt;
&lt;li&gt;スコア・ストリーク・バッジの実装&lt;/li&gt;
&lt;li&gt;SNSシェアカード生成 (Flutter Web → PNG → base64)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;を紹介します。&lt;/p&gt;




&lt;h2&gt;
  
  
  AI大学の概要
&lt;/h2&gt;

&lt;p&gt;ユーザーが各AIプロバイダーの概要・モデル一覧・API情報・最新ニュースを学習し、クイズに答えることで「AI偏差値」を競う機能です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│ AI大学                                           │
│                                                  │
│  [Google] [OpenAI] [Anthropic] [Meta] [xAI] ... │
│   34社タブで切り替え                              │
│                                                  │
│  📖 概要 | 🤖 モデル | 🔌 API | 📰 最新ニュース  │
│                                                  │
│  [クイズに挑戦] ✓ 3/5問正解                      │
│  🔥 連続学習 7日目                               │
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  DBスキーマ
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- コンテンツテーブル&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ai_university_content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'google', 'openai', ...&lt;/span&gt;
  &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'overview', 'models', 'api', 'news'&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- Markdown形式&lt;/span&gt;
  &lt;span class="n"&gt;published_at&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;-- UPSERT用&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- スコアテーブル&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ai_university_scores&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;quiz_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;studied_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quiz_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- ストリークテーブル&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ai_university_streaks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;current_streak&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;max_streak&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_studied_date&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RLS で &lt;code&gt;user_id = auth.uid()&lt;/code&gt; を設定し、自分のスコアのみ読み書き可能。&lt;/p&gt;




&lt;h2&gt;
  
  
  34社のプロバイダー一覧
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;メガプレイヤー (9社)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;google, openai, anthropic, microsoft, meta,&lt;/span&gt;
  &lt;span class="s"&gt;x (xAI/Grok), deepseek, mistral, perplexity&lt;/span&gt;

&lt;span class="na"&gt;特化型AI (11社)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;groq, cohere, amazon, oracle, reka,&lt;/span&gt;
  &lt;span class="s"&gt;aleph_alpha, together_ai, fireworks_ai, replicate,&lt;/span&gt;
  &lt;span class="s"&gt;writer, ai21&lt;/span&gt;

&lt;span class="na"&gt;AIインフラ層 (5社)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;voyage, elevenlabs, openrouter, ollama, ideogram&lt;/span&gt;

&lt;span class="na"&gt;マルチモーダル (5社)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;runway, suno, udio, luma, kling&lt;/span&gt;

&lt;span class="na"&gt;その他 (4社)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;pika, stability, huggingface, ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;各プロバイダーには &lt;code&gt;overview&lt;/code&gt; / &lt;code&gt;models&lt;/code&gt; / &lt;code&gt;api&lt;/code&gt; / &lt;code&gt;news&lt;/code&gt; の4カテゴリのコンテンツがあります。&lt;/p&gt;




&lt;h2&gt;
  
  
  コンテンツ自動更新: 2層アーキテクチャ
&lt;/h2&gt;

&lt;p&gt;コンテンツ更新は2つの仕組みが並行して動いています:&lt;/p&gt;

&lt;h3&gt;
  
  
  層1: GitHub Actions (2時間ごと・RSS駆動)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ai-university-update.yml&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update news content&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;# 各プロバイダーの公式ブログRSSを取得&lt;/span&gt;
      &lt;span class="s"&gt;# schedule-hub EF の upsert_news action で更新&lt;/span&gt;
      &lt;span class="s"&gt;curl -X POST \&lt;/span&gt;
        &lt;span class="s"&gt;"https://{project}.supabase.co/functions/v1/schedule-hub" \&lt;/span&gt;
        &lt;span class="s"&gt;-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \&lt;/span&gt;
        &lt;span class="s"&gt;-d '{"action":"ai_university.upsert_news","provider":"google","content":"..."}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  層2: Claude Code Schedule (4時間ごと・NotebookLM Deep Research)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# NotebookLM に最新AIニュースを調査させて高品質コンテンツを生成&lt;/span&gt;
notebooklm use jibun-master-brain
notebooklm &lt;span class="nb"&gt;source &lt;/span&gt;add-research &lt;span class="s2"&gt;"Google Gemini OpenAI GPT Anthropic Claude latest 2026"&lt;/span&gt;
notebooklm research &lt;span class="nb"&gt;wait
&lt;/span&gt;notebooklm ask &lt;span class="s2"&gt;"各AIプロバイダーの最新情報をまとめて"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions の RSS 更新より深い情報を Claude Schedule が上書きするため、&lt;br&gt;
後から書いた方が最新版になります。&lt;/p&gt;


&lt;h2&gt;
  
  
  Flutter側の実装: DB駆動タブ
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// gemini_university_v2_page.dart&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_GeminiUniversityV2PageState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GeminiUniversityV2Page&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;TickerProviderStateMixin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;TabController&lt;/span&gt; &lt;span class="n"&gt;_tabController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_providers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_loadProviders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_loadProviders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Supabase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;client&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ai_university_content'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'overview'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;providers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_providers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;_tabController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TabController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;length:&lt;/span&gt; &lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;vsync:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;_providers&lt;/code&gt; は DB から動的に取得するため、新プロバイダーを migration で追加するだけでタブが自動追加されます。&lt;/p&gt;


&lt;h2&gt;
  
  
  スコア書き込み (RLS直接UPSERT)
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// クイズ回答時&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Supabase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;client&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ai_university_scores'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="s"&gt;'user_id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'provider'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'quiz_id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;quizId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'correct'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;isCorrect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'studied_at'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toIso8601String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nl"&gt;onConflict:&lt;/span&gt; &lt;span class="s"&gt;'user_id,provider,quiz_id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;RLS ポリシー:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- INSERT: 自分のスコアのみ&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Users can insert own scores"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ai_university_scores&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  SNSシェアカード生成 (Flutter Web)
&lt;/h2&gt;

&lt;p&gt;学習完了時に「何社学習済み」をビジュアル化してシェアできます:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RenderRepaintBoundary → PNG → base64 → HTMLAnchorElement&lt;/span&gt;
&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_shareProgress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;boundary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_shareCardKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentContext&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findRenderObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;RenderRepaintBoundary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;boundary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pixelRatio:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;byteData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toByteData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;format:&lt;/span&gt; &lt;span class="n"&gt;ImageByteFormat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;png&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Web: base64エンコードしてダウンロードリンクを生成&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byteData&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asUint8List&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HTMLAnchorElement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'data:image/png;base64,&lt;/span&gt;&lt;span class="si"&gt;$base64&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'ai-university-progress.png'&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  連続学習ストリーク (Supabase RPC)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- update_streak RPC&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;update_ai_university_streak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_streak&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_streak&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="n"&gt;v_last_date&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;v_current&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;v_max&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;last_studied_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_streak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_streak&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_last_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_max&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_university_streaks&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;v_last_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;v_current&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 連続&lt;/span&gt;
  &lt;span class="n"&gt;ELSIF&lt;/span&gt; &lt;span class="n"&gt;v_last_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 今日すでに学習済み&lt;/span&gt;
  &lt;span class="k"&gt;ELSE&lt;/span&gt;
    &lt;span class="n"&gt;v_current&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- リセット&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;v_max&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GREATEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;ai_university_streaks&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;current_streak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_streak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;last_studied_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v_current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="k"&gt;DEFINER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;項目&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;対応プロバイダー&lt;/td&gt;
&lt;td&gt;34社&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;コンテンツ更新&lt;/td&gt;
&lt;td&gt;2時間ごと (GitHub Actions) + 4時間ごと (Claude Schedule)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;スコア管理&lt;/td&gt;
&lt;td&gt;Supabase &lt;code&gt;ai_university_scores&lt;/code&gt; (RLS直接UPSERT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ストリーク&lt;/td&gt;
&lt;td&gt;Supabase RPC &lt;code&gt;update_ai_university_streak&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;バッジ&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ai_university_badges&lt;/code&gt; テーブル (EF自動発行)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;シェア&lt;/td&gt;
&lt;td&gt;Flutter Web → PNG → base64 (package:web)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AIプロバイダーの乱立は個人開発者にとってノイズですが、「全部学べる1アプリ」として整理すると差別化になりました。&lt;/p&gt;

&lt;p&gt;フィードバックお待ちしています。&lt;/p&gt;




&lt;p&gt;URL: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Flutter #Supabase #AI #buildinpublic #個人開発
&lt;/h1&gt;

</description>
      <category>flutter</category>
      <category>supabase</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Claude Code 4インスタンス並列開発 + Edge Functions全量CI/CDデプロイを整備した話</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Sun, 12 Apr 2026 09:01:32 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/claude-code-4insutansubing-lie-kai-fa-edge-functionsquan-liang-cicddepuroiwozheng-bei-sitahua-5goa</link>
      <guid>https://forem.com/kanta13jp1/claude-code-4insutansubing-lie-kai-fa-edge-functionsquan-liang-cicddepuroiwozheng-bei-sitahua-5goa</guid>
      <description>&lt;h1&gt;
  
  
  ブログ下書き 2026-03-28 (Edge Functions CI/CD)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  タイトル案
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Claude Code 4インスタンス並列開発 + Edge Functions全量CI/CDデプロイを整備した話&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自分株式会社 開発日記 #10: Web/VSCode/Windows/PowerShell 4インスタンス協調開発&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FlutterWeb×Supabase: Edge Functions 36本を全てCI/CDに乗せた話&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  投稿先候補
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[x] Zenn (技術詳細系)&lt;/li&gt;
&lt;li&gt;[x] Qiita (実用系)&lt;/li&gt;
&lt;li&gt;[ ] note (エッセイ系)&lt;/li&gt;
&lt;li&gt;[ ] dev.to (英語版)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  本文下書き（1500〜2000字）
&lt;/h2&gt;

&lt;h3&gt;
  
  
  はじめに
&lt;/h3&gt;

&lt;p&gt;自分株式会社（Flutter Web + Supabase）は、Notion・Evernote・MoneyForward・X など21の競合SaaSを超える統合プラットフォームを1人で開発中です。&lt;/p&gt;

&lt;p&gt;今日は &lt;strong&gt;Claude Code を4インスタンス同時実行&lt;/strong&gt;する開発体制を構築し、Edge Functions の CI/CD を完備した話をします。&lt;/p&gt;

&lt;h3&gt;
  
  
  4インスタンス並列開発の役割分担
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VSCode版（Claude Code IDE）  → lib/ フロントエンド実装
Web版（claude.ai/code）       → supabase/functions/ Edge Functions
Windows版（デスクトップアプリ）  → docs/ ドキュメント・マイグレーション
PowerShell（ターミナル）         → 全体管理・CI監視
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;各インスタンスが担当ディレクトリを守り、開始前に &lt;code&gt;git pull --rebase origin main&lt;/code&gt; を必ず実行することで競合を防いでいます。&lt;/p&gt;

&lt;h3&gt;
  
  
  問題: Edge Functions がCI/CDに含まれていなかった
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;deploy-prod.yml&lt;/code&gt; を確認すると、以下の7つの Edge Functions がデプロイ対象に含まれていませんでした:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;check-competitor-updates&lt;/code&gt; — 競合14社のWeb可用性チェック&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;health-check&lt;/code&gt; — DB接続・テーブル可用性チェック&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;analyze-reality&lt;/code&gt; — リアリティチェックAI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trigger-analysis&lt;/code&gt; — 選挙分析トリガー&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;local-election-intelligence&lt;/code&gt; — 地方選挙インテリジェンス&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent-runtime-cycle&lt;/code&gt; — AIエージェント定期サイクル&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;これらは手動デプロイが必要な状態で、CI/CDの恩恵を受けられていませんでした。&lt;/p&gt;

&lt;h3&gt;
  
  
  解決: deploy-prod.yml に全関数を追加
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Supabase Edge Functions&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;# 既存の関数...&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy reply-support-request --no-verify-jwt&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy post-x-update --no-verify-jwt&lt;/span&gt;
    &lt;span class="s"&gt;# 今回追加&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy check-competitor-updates --no-verify-jwt&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy get-competitor-monitoring --no-verify-jwt&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy health-check --no-verify-jwt&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy analyze-reality --no-verify-jwt&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy trigger-analysis --no-verify-jwt&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy local-election-intelligence --no-verify-jwt&lt;/span&gt;
    &lt;span class="s"&gt;supabase functions deploy agent-runtime-cycle --no-verify-jwt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  新規作成: get-competitor-monitoring Edge Function
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;check-competitor-updates&lt;/code&gt; は競合サイトをPOSTで叩いてDBに記録しますが、&lt;br&gt;
管理者UIからそのデータを参照するGETエンドポイントがありませんでした。&lt;/p&gt;

&lt;p&gt;新規作成した &lt;code&gt;get-competitor-monitoring&lt;/code&gt; の設計:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GET /functions/v1/get-competitor-monitoring?days=7&amp;amp;limit=50&lt;/span&gt;
&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;days&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;50&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;competitor_monitoring&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checked_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checked_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 競合ごとの最新結果をまとめる&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latestByCompetitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;latestByCompetitor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;competitor_key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;latestByCompetitor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;competitor_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;competitor_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;competitor_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;checked_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;competitors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;latestByCompetitor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;availabilityPct&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;役割分担の明確化:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;check-competitor-updates&lt;/code&gt; (POST): 実際にサイトを叩いてDBに記録&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get-competitor-monitoring&lt;/code&gt; (GET): DBから最新結果を取得してUIに提供&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  EdgeFunctionSummaryCard による UI カバレッジ可視化
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lib/widgets/edge_function_summary_card.dart&lt;/code&gt; には全36 Edge Functions の UI 接続状況が一覧で表示されます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Edge Functions 実装状況
全 35 件 | UI 実装済: 31 件 | UI未実装: 4 件 (89%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UI 未実装の関数（サーバーサイド専用を除く）には警告アイコンが表示され、&lt;br&gt;
詳細ページ（&lt;code&gt;/edge-functions&lt;/code&gt;）から手動テストも可能です。&lt;/p&gt;

&lt;h3&gt;
  
  
  flutter analyze 0件維持の重要性
&lt;/h3&gt;

&lt;p&gt;4インスタンスが並列で &lt;code&gt;lib/&lt;/code&gt; を変更すると &lt;code&gt;flutter analyze&lt;/code&gt; のエラーが増えるリスクがあります。&lt;br&gt;
Web インスタンスは &lt;code&gt;supabase/functions/&lt;/code&gt; のみを担当し、&lt;code&gt;lib/&lt;/code&gt; には触れないことで Dart 解析エラーの発生を抑えています。&lt;/p&gt;

&lt;h3&gt;
  
  
  まとめ
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Edge Functions 36本全てを CI/CD に乗せ完備&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;新規 &lt;code&gt;get-competitor-monitoring&lt;/code&gt;&lt;/strong&gt; で管理者 UI に競合可用性データを提供&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4インスタンス並列開発体制&lt;/strong&gt; でフロント/バックエンド/ドキュメントを同時進行&lt;/li&gt;
&lt;li&gt;CI/CD の恩恵を受けられていなかった7関数を &lt;code&gt;deploy-prod.yml&lt;/code&gt; に追加&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;サービスURL: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;自分株式会社&lt;/a&gt;&lt;br&gt;
タグ: #FlutterWeb #Supabase #buildinpublic #ClaudeCode #EdgeFunctions&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>supabase</category>
      <category>claudecode</category>
      <category>edgefunction</category>
    </item>
    <item>
      <title>Claude Code Schedule で CS・バグ修正・競合モニタリングを完全自動化した話</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Sun, 12 Apr 2026 09:01:30 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/claude-code-schedule-de-csbaguxiu-zheng-jing-he-monitaringuwowan-quan-zi-dong-hua-sitahua-1ia1</link>
      <guid>https://forem.com/kanta13jp1/claude-code-schedule-de-csbaguxiu-zheng-jing-he-monitaringuwowan-quan-zi-dong-hua-sitahua-1ia1</guid>
      <description>&lt;h1&gt;
  
  
  Claude Code Schedule で CS・バグ修正・競合モニタリングを完全自動化した話
&lt;/h1&gt;

&lt;h2&gt;
  
  
  タイトル案
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Claude Code Schedule で CS・バグ修正・競合モニタリングを完全自動化した話&lt;/li&gt;
&lt;li&gt;PCもサーバーも不要: Claude Code Schedule で9つの定期タスクを無料自動化&lt;/li&gt;
&lt;li&gt;Flutter Web + Supabase アプリの運用を Claude Code Schedule で完全自動化する方法&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  投稿先候補
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[x] Zenn (技術実装メイン)&lt;/li&gt;
&lt;li&gt;[ ] Qiita (実用Tips)&lt;/li&gt;
&lt;li&gt;[ ] dev.to (英語版)&lt;/li&gt;
&lt;li&gt;[ ] note (エッセイ)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  本文下書き
&lt;/h2&gt;

&lt;h3&gt;
  
  
  はじめに
&lt;/h3&gt;

&lt;p&gt;個人開発アプリ「自分株式会社」の運用を Claude Code Schedule で完全自動化した。&lt;/p&gt;

&lt;p&gt;CS対応、バグ修正、競合モニタリング、日次レポート、X投稿、PRレビュー、インフラ監視、脆弱性チェック、ブログ下書き生成。合計9つのタスクを、PCを起動せずに定期実行している。&lt;/p&gt;

&lt;h3&gt;
  
  
  技術スタック
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;フロントエンド&lt;/strong&gt;: Flutter Web (Dart)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;バックエンド&lt;/strong&gt;: Supabase (PostgreSQL + Edge Functions / Deno)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ホスティング&lt;/strong&gt;: Firebase Hosting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: GitHub Actions (push to main → 自動デプロイ)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;自動化&lt;/strong&gt;: Claude Code Schedule&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  実装のポイント
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. CLAUDE.md にタスク定義を書くだけ
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Task: cs-check (毎時 実行)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; WebFetch で GET /functions/v1/get-support-tickets
&lt;span class="p"&gt;2.&lt;/span&gt; FAQ で答えられる → 返信
&lt;span class="p"&gt;3.&lt;/span&gt; バグの可能性 → ソースを読んで修正 → git commit → push
&lt;span class="p"&gt;4.&lt;/span&gt; 判断困難 → エスカレーション
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Edge Function で薄い API を用意
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// get-support-tickets: 未返信チケット + FAQ を返す&lt;/span&gt;
&lt;span class="c1"&gt;// reply-support-request: 返信 or エスカレーション&lt;/span&gt;
&lt;span class="c1"&gt;// health-check: DB接続性・テーブル可用性チェック&lt;/span&gt;
&lt;span class="c1"&gt;// check-competitor-updates: 競合21社 HEAD リクエスト&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. 実行ログを schedule_task_runs テーブルに記録
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;schedule_task_runs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'running'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;管理者ダッシュボードの ScheduleTaskMonitorCard で実行状況を確認できる。&lt;/p&gt;

&lt;h3&gt;
  
  
  9つの自動化タスク
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;タスク&lt;/th&gt;
&lt;th&gt;頻度&lt;/th&gt;
&lt;th&gt;やること&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;daily-report&lt;/td&gt;
&lt;td&gt;毎日 09:00&lt;/td&gt;
&lt;td&gt;メトリクス取得 → レポート生成 → X投稿 → 競合チェック&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cs-check&lt;/td&gt;
&lt;td&gt;毎時&lt;/td&gt;
&lt;td&gt;チケット確認 → FAQ返信 / バグ修正 / エスカレーション&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;weekly-sns-draft&lt;/td&gt;
&lt;td&gt;毎週月曜&lt;/td&gt;
&lt;td&gt;週次実績サマリー → SNS投稿ドラフト&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;daily-development&lt;/td&gt;
&lt;td&gt;毎日 10:00&lt;/td&gt;
&lt;td&gt;ロードマップに沿って開発を推進&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pr-auto-review&lt;/td&gt;
&lt;td&gt;3時間毎&lt;/td&gt;
&lt;td&gt;open PR のコードレビュー&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;competitor-monitoring&lt;/td&gt;
&lt;td&gt;毎日 07:00&lt;/td&gt;
&lt;td&gt;競合21社のWebサイト・ニュースチェック&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;infra-health-check&lt;/td&gt;
&lt;td&gt;毎時 30分&lt;/td&gt;
&lt;td&gt;DB・Firebase Hosting の可用性確認&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dependency-audit&lt;/td&gt;
&lt;td&gt;毎週月曜&lt;/td&gt;
&lt;td&gt;Flutter/Deno 依存パッケージの脆弱性チェック&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blog-draft&lt;/td&gt;
&lt;td&gt;毎日 08:00&lt;/td&gt;
&lt;td&gt;技術ブログ下書き自動生成&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  詰まったポイント
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schedule サンドボックスの制約&lt;/strong&gt;: SSH不可、DB直接接続不可。すべて WebFetch (HTTP) 経由で操作する必要がある → Edge Function を薄い API として用意することで解決&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;同一周期の制約&lt;/strong&gt;: 同じ cron 式は1つしか設定できない → タスクを統合するか、異なる分オフセットで対応&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;RLS との戦い&lt;/strong&gt;: Schedule からの書き込みは service_role で行うため、RLS ポリシーを service_role 用に設定する必要がある&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  まとめ
&lt;/h3&gt;

&lt;p&gt;Claude Code Schedule を使えば:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PC不要、サーバー不要、API代金不要&lt;/li&gt;
&lt;li&gt;CLAUDE.md を書き換えるだけで動作を変更&lt;/li&gt;
&lt;li&gt;CS対応、バグ修正、モニタリング、レポート、ブログ下書きまで自動化&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pro プラン以上を契約しているなら追加費用ゼロで使える。&lt;/p&gt;




&lt;p&gt;URL: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/kanta13jp1/my_web_app" rel="noopener noreferrer"&gt;https://github.com/kanta13jp1/my_web_app&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  FlutterWeb #Supabase #ClaudeCode #buildinpublic
&lt;/h1&gt;

</description>
      <category>claudecode</category>
      <category>flutter</category>
      <category>supabase</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>GitHub Actions Supabase で技術記事投稿を自動化した話 — 1日に21本をQiita/dev.toへ</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Sun, 12 Apr 2026 07:48:11 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/github-actions-x-supabase-deji-shu-ji-shi-tou-gao-wozi-dong-hua-sitahua-1ri-ni21ben-woqiitadevtohe-4fb3</link>
      <guid>https://forem.com/kanta13jp1/github-actions-x-supabase-deji-shu-ji-shi-tou-gao-wozi-dong-hua-sitahua-1ri-ni21ben-woqiitadevtohe-4fb3</guid>
      <description>&lt;h1&gt;
  
  
  GitHub Actions × Supabase で技術記事投稿を自動化した話 — 1日に21本をQiita/dev.toへ
&lt;/h1&gt;

&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;個人開発アプリ「自分株式会社」の開発を続けながら、技術記事を書くのが後回しになっていました。&lt;/p&gt;

&lt;p&gt;気づいたら &lt;code&gt;docs/blog-drafts/&lt;/code&gt; に未公開のドラフトが21本も溜まっていました。&lt;/p&gt;

&lt;p&gt;この記事では、&lt;strong&gt;1コマンドでQiita/dev.toへ自動投稿するGitHub Actionsワークフロー&lt;/strong&gt;を実装して、21本を1セッションで一括公開した話をまとめます。&lt;/p&gt;




&lt;h2&gt;
  
  
  問題: ドラフトが溜まる一方
&lt;/h2&gt;

&lt;p&gt;Flutter Web + Supabase の個人開発では機能追加のたびにブログのネタができます。しかし投稿作業が手間で、ドラフトは増えるが公開されない状態が続いていました:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/blog-drafts/
├── 2026-03-28-note-comments.md       # 未公開
├── 2026-03-31-app-feedback.md        # 未公開
├── 2026-04-01-workflow-automation.md # 未公開
... (21本)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  解決策: &lt;code&gt;blog-publish.yml&lt;/code&gt; — 1コマンド投稿
&lt;/h2&gt;

&lt;p&gt;GitHub Actions のワークフローを作り、&lt;code&gt;gh workflow run&lt;/code&gt; で任意のドラフトをQiita/dev.toへ投稿できるようにしました。&lt;/p&gt;

&lt;h3&gt;
  
  
  ワークフロー全体像
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Blog Publish (技術記事投稿)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;draft_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;投稿するドラフトのパス'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;platforms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;qiita,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;devto,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;または&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;qiita,devto'&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;qiita,devto'&lt;/span&gt;
      &lt;span class="na"&gt;dry_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true=投稿しない,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;false=実投稿'&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5つのステップ&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Step 2&lt;/strong&gt;: frontmatter からタイトル・タグを抽出&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3&lt;/strong&gt;: Supabase &lt;code&gt;blog_posts&lt;/code&gt; テーブルに登録&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4&lt;/strong&gt;: &lt;code&gt;schedule-hub&lt;/code&gt; EF 経由で Qiita / dev.to へ投稿&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 5&lt;/strong&gt;: &lt;code&gt;published: false&lt;/code&gt; → &lt;code&gt;published: true&lt;/code&gt; に更新してブランチ作成&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 6&lt;/strong&gt;: 実行ログを &lt;code&gt;schedule_task_runs&lt;/code&gt; に記録&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  投稿コマンド (1行)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh workflow run blog-publish.yml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;draft_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docs/blog-drafts/2026-04-12-topic.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;platforms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"qiita,devto"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Supabase Edge Function で API を統合
&lt;/h2&gt;

&lt;p&gt;Qiita と dev.to への投稿は &lt;code&gt;schedule-hub&lt;/code&gt; という Supabase Edge Function が担います。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// schedule-hub/index.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicActions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blog.auto_publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blog.create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blog.auto_publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qiita&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qiita&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;publishToQiita&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;stripFrontmatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;devto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;publishToDevTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;stripFrontmatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ポイント&lt;/strong&gt;: &lt;code&gt;publicActions&lt;/code&gt; 配列に入れることで SERVICE_ROLE_KEY での呼び出し時の JWT 認証をスキップ。GitHub Actions から直接呼べます。&lt;/p&gt;

&lt;h3&gt;
  
  
  frontmatter の自動除去
&lt;/h3&gt;

&lt;p&gt;Markdown ドラフトには Zenn や Qiita 用の frontmatter が含まれています。Edge Function 側で &lt;code&gt;stripFrontmatter()&lt;/code&gt; を実行して本文だけを各プラットフォームに送ります:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stripFrontmatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;---&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;---&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  frontmatter の互換性
&lt;/h2&gt;

&lt;p&gt;Zenn形式 (&lt;code&gt;topics:&lt;/code&gt;) と Qiita形式 (&lt;code&gt;tags:&lt;/code&gt;) の両方に対応しています:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 2 での抽出スクリプト&lt;/span&gt;
&lt;span class="nv"&gt;TAGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(tags|topics):'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRAFT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^tags: *//;s/^topics: *//'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[]"'&lt;/span&gt;&lt;span class="s2"&gt;"'"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^ *//;s/ *$//'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'^$'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/,$//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TAGS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;TAGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Flutter,Supabase,buildinpublic"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;形式&lt;/th&gt;
&lt;th&gt;例&lt;/th&gt;
&lt;th&gt;動作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qiita形式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tags: Flutter,Supabase,AI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ そのまま抽出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zenn形式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;topics: ["Flutter", "Supabase"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ 変換して抽出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配列形式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tags: [Flutter, Supabase, DNS]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ 変換して抽出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;タグなし&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅ デフォルト値を使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 5 の制約: GITHUB_TOKEN とブランチ保護
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;published: true&lt;/code&gt; への更新は Step 5 でブランチに push されますが、&lt;code&gt;GITHUB_TOKEN&lt;/code&gt; はブランチ保護 (require PR) をバイパスできません:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;結果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git push origin HEAD:main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ GH006 Protected branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gh api repos/.../merges POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ HTTP 409&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gh pr create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ GitHub Actions は PR 作成不可&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;現在の運用&lt;/strong&gt;: Step 5 がブランチ &lt;code&gt;blog-publish/YYYYMMDDHHMMSS&lt;/code&gt; を作成 → ローカルで &lt;code&gt;git merge --no-edit &amp;amp;&amp;amp; git push origin main&lt;/code&gt; でマージ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;恒久解決策&lt;/strong&gt;: リポジトリ設定で &lt;code&gt;BLOG_PAT&lt;/code&gt; シークレット (bypass 権限付き PAT) を設定すれば完全自動化できます。&lt;/p&gt;




&lt;h2&gt;
  
  
  21本を1日で一括公開した手順
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 未公開ドラフトを一覧&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"^published: false"&lt;/span&gt; docs/blog-drafts/ | &lt;span class="nb"&gt;sort&lt;/span&gt;

&lt;span class="c"&gt;# 3本ずつ並行 dispatch&lt;/span&gt;
gh workflow run blog-publish.yml &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;draft_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;platforms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"qiita,devto"&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;
gh workflow run blog-publish.yml &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;draft_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;platforms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"qiita,devto"&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;
gh workflow run blog-publish.yml &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;draft_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;platforms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"qiita,devto"&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;

&lt;span class="c"&gt;# 完了後、published:true ブランチをまとめてマージ&lt;/span&gt;
git fetch origin
git merge origin/blog-publish/20260412-XXXXXX &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3本並行 dispatch × 7セット = 21本を約1時間で公開完了。&lt;/p&gt;




&lt;h2&gt;
  
  
  実装のポイントまとめ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;課題&lt;/th&gt;
&lt;th&gt;解決策&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qiita タグ空で 403&lt;/td&gt;
&lt;td&gt;デフォルトタグを設定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions 認証 (401)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;publicActions&lt;/code&gt; で JWT スキップ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zenn/Qiita frontmatter 混在&lt;/td&gt;
&lt;td&gt;`grep -E '^(tags\&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ブランチ保護でマージ不可&lt;/td&gt;
&lt;td&gt;ローカルマージで回避&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev.to 日本語タイトル&lt;/td&gt;
&lt;td&gt;UTF-8 エンコード済み URL で投稿成功&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;p&gt;{% raw %}&lt;code&gt;blog-publish.yml&lt;/code&gt; + Supabase &lt;code&gt;schedule-hub&lt;/code&gt; EF の組み合わせで、ドラフトから投稿まで1コマンドで完了するワークフローを構築しました。&lt;/p&gt;

&lt;p&gt;溜まっていた21本のドラフトを1セッションで一括公開でき、Qiita フォロワー数も増加中です。&lt;/p&gt;

&lt;p&gt;技術記事は書きっぱなしにせず、&lt;strong&gt;自動化で公開のハードルを下げる&lt;/strong&gt;ことが継続の鍵だと実感しています。&lt;/p&gt;

&lt;p&gt;フィードバックお待ちしています。&lt;/p&gt;




&lt;p&gt;URL: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  GitHub #Supabase #GitHubActions #buildinpublic #個人開発
&lt;/h1&gt;

</description>
      <category>github</category>
      <category>supabase</category>
      <category>githubactions</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>FlutterアプリのノートAI検索をOpenAIなしでも動くように安定化した話</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Sun, 12 Apr 2026 07:42:21 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/flutterapurinonotoaijian-suo-woopenainasidemodong-kuyounian-ding-hua-sitahua-hnj</link>
      <guid>https://forem.com/kanta13jp1/flutterapurinonotoaijian-suo-woopenainasidemodong-kuyounian-ding-hua-sitahua-hnj</guid>
      <description>&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;自分株式会社（Notion・Evernote・MoneyForward・Slack を1つに統合するAIライフマネジメントアプリ）の開発を続けています。&lt;/p&gt;

&lt;p&gt;今日は「&lt;strong&gt;AI 検索の安定化&lt;/strong&gt;」を実装しました。&lt;/p&gt;

&lt;p&gt;元々 OpenAI API を使ったセマンティック検索機能はあったのですが、OpenAI キーが設定されていない環境や API 障害時には検索が完全に使えない状態でした。&lt;br&gt;
これを「常に動く全文検索」にしながら「可能なら AI ランキングも使う」というハイブリッド構成に改善しました。&lt;/p&gt;
&lt;h2&gt;
  
  
  課題：AI 検索が脆すぎた
&lt;/h2&gt;

&lt;p&gt;元の &lt;code&gt;ai-search&lt;/code&gt; Edge Function は以下の問題を抱えていました：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 問題のある元実装&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openaiApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;openaiApiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OpenAI API key not configured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// ← キーがなければ即エラー&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenAI が使えない状況が1つでも発生すると、ユーザーは完全に検索できなくなります。&lt;/p&gt;

&lt;h2&gt;
  
  
  解決策：テキスト検索フォールバック + モード制御
&lt;/h2&gt;

&lt;p&gt;改善後の構成はこうなりました：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;リクエスト (mode: 'auto')
  ↓
OpenAI 設定済み？
  Yes → AI ランキング検索
    → 失敗時 → ILIKE テキスト検索にフォールバック
  No  → ILIKE テキスト検索
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PostgreSQL ILIKE ベースの全文検索
&lt;/h3&gt;

&lt;p&gt;まず日本語でも確実に動く検索を実装します。&lt;br&gt;
PostgreSQL の &lt;code&gt;ILIKE&lt;/code&gt; はバイト列で部分一致するので日本語でも問題なく使えます：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;textSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="c1"&gt;// スペース区切りの全キーワードを OR 検索&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orConditions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`title.ilike.%&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%,content.ilike.%&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id, title, content, tags, category_id, created_at, updated_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deleted_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orConditions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updated_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  AI フォールバック付きメイン処理
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;useAi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rankedNotes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;explanation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;aiRank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;openaiApiKey&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;allNotes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rankedNotes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;searchMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aiError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// AI 失敗 → テキスト検索にフォールバック&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AI search failed, falling back:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aiError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;textSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supabaseClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;searchMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text_fallback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// OpenAI 未設定: 最初からテキスト検索&lt;/span&gt;
  &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;textSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supabaseClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;searchMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;レスポンスに &lt;code&gt;searchMode&lt;/code&gt; フィールドを追加することで、フロントエンドがどのモードで検索したかを表示できます。&lt;/p&gt;

&lt;h2&gt;
  
  
  Flutter 側の対応
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AiSearchPage&lt;/code&gt; を更新して、検索モードを UI に表示するようにしました：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 検索モードバッジ表示&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_searchMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_searchMode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'ai'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;auto_awesome&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text_fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;size:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;_searchMode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'ai'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFF6366F1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grey&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_searchMode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'ai'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;'AI 検索'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_searchMode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'text_fallback'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;'テキスト検索（AIフォールバック）'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="s"&gt;'テキスト検索'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ホーム画面に検索カードを追加
&lt;/h2&gt;

&lt;p&gt;毎回「AIノート検索」メニューまで辿らなくても済むよう、ホーム画面に &lt;code&gt;NoteSearchCard&lt;/code&gt; を追加しました：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoteSearchCard&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;InkWell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AiSearchPage&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;manage_search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFF6366F1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
              &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ノートを検索'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'キーワード・自然言語で全ノートを横断検索'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  設計の考え方
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;優先度&lt;/th&gt;
&lt;th&gt;何を守るか&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;常に動く&lt;/strong&gt; (テキスト検索は常に使える)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;AI で価値を加える&lt;/strong&gt; (設定済みなら AI ランキング)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;透明性&lt;/strong&gt; (どのモードで動いたか UI に表示)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;これは Notion の全文検索に近いシンプルさを提供しながら、将来的に pgvector や Claude API によるセマンティック検索への拡張余地も残しています。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI 必須 → テキスト検索フォールバック付きハイブリッド構成に変更&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mode: 'auto'&lt;/code&gt; でクライアントは何も考えなくていい設計&lt;/li&gt;
&lt;li&gt;ホーム画面検索カードで UX を改善&lt;/li&gt;
&lt;li&gt;deno lint 0 件、flutter analyze 0 件を維持&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;次は pgvector の &lt;code&gt;notes_embedding&lt;/code&gt; カラム追加による本格的なベクトル検索を検討中です。&lt;/p&gt;

&lt;p&gt;サービス: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  FlutterWeb #Supabase #buildinpublic
&lt;/h1&gt;

</description>
      <category>flutter</category>
      <category>supabase</category>
      <category>deno</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
