Skip to main content

I. Overall Architecture

Scheme A: Direct Connection Mode (Default)

Core Principle: The frontend does not directly call WeChat API. All WeChat API calls are proxied through Supabase Edge Functions to avoid exposing AppSecret on the client side. Users can choose either direct connection mode or reverse proxy mode to solve the IP whitelist problem.

II. Prerequisites

2.1 WeChat Developer Platform Configuration

Complete the following operations on the WeChat Developer Platform:
  1. Obtain Developer Credentials
    • Visit the WeChat Developer Platform
    • Select the corresponding Official Account
    • View or reset in “Basic Information”
    • Record AppID (Developer ID)
    • Record or reset AppSecret (Developer Secret)
  2. Configure IP Whitelist
    • Add Edge Function egress IP in “Basic Information → Developer Secret → API IP Whitelist”
    • ⚠️ Important: Supabase Edge Functions use dynamic IPs. The first call will return errcode: 40164. Extract the IP from the error message and add it to the whitelist
    • Extraction method: Parse invalid ip x.x.x.x from errmsg
  3. Confirm Official Account Type
    • Verified service accounts have full API permissions
    • Subscription accounts have limited API access (e.g., custom menus, data statistics)

2.2 Supabase Project Configuration

  1. Environment Variables / Edge Function Secrets Configure the following Secrets (via Supabase Dashboard or CLI):
    Secret NameDescriptionExample
    SUPERUN_WECHAT_APP_IDOfficial Account AppIDwx1234567890abcdef
    SUPERUN_WECHAT_APP_SECRETOfficial Account AppSecreta1b2c3d4e5f6...
    SUPERUN_WECHAT_API_PROXY_URLReverse proxy address (optional)https://wechat-proxy.example.com
    SUPABASE_URLSupabase project URL (built-in)https://xxxxx.supabase.co
    SUPABASE_ANON_KEYSupabase anonymous key (built-in)eyJhbG...
    SUPABASE_SERVICE_ROLE_KEYSupabase service role key (built-in)eyJhbG...
    SUPABASE_URL, SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY are built-in Supabase variables and do not need manual configuration. SUPERUN_WECHAT_API_PROXY_URL is optional. If not configured, it will directly connect to WeChat API (requires manual IP whitelist maintenance). If configured, all WeChat requests will go through the proxy server.
  2. Edge Function Configuration (supabase/config.toml)
    project_id = "<your-project-id>"
    [functions.wechat-token]
    verify_jwt = false
    [functions.wechat-data]
    verify_jwt = false
    [functions.wechat-articles]
    verify_jwt = false
    [functions.wechat-menu]
    verify_jwt = false
    
    verify_jwt = false allows Edge Functions to call each other. Authentication logic is manually implemented by parsing JWT inside the function.

III. Database Table Structure

3.1 wechat_tokens — Token Cache Table

CREATE TABLE wechat_tokens (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  access_token TEXT NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Purpose: Cache WeChat access_token to avoid calling the WeChat token API on every request (2-hour validity, limited daily calls).

3.2 articles — Article Content Table

CREATE TABLE articles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL,
  title TEXT NOT NULL DEFAULT '',
  content TEXT NOT NULL DEFAULT '',
  author TEXT NOT NULL DEFAULT '',
  digest TEXT NOT NULL DEFAULT '',
  thumb_url TEXT NOT NULL DEFAULT '',
  status TEXT NOT NULL DEFAULT 'draft',       -- draft / published / deleted
  wechat_media_id TEXT NOT NULL DEFAULT '',    -- WeChat-side article_id
  wechat_article_url TEXT NOT NULL DEFAULT '', -- Article URL after publishing
  published_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

3.3 menu_configs — Menu Configuration Table

CREATE TABLE menu_configs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL,
  menu_data JSONB NOT NULL DEFAULT '{}',    -- WeChat menu JSON structure
  version INTEGER NOT NULL DEFAULT 1,
  status TEXT NOT NULL DEFAULT 'draft',     -- draft / published / deleted
  published_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

3.4 auto_reply_rules — Auto Reply Rules Table

CREATE TABLE auto_reply_rules (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL,
  rule_type TEXT NOT NULL DEFAULT 'keyword',    -- keyword / subscribe / default
  keyword TEXT NOT NULL DEFAULT '',
  match_type TEXT NOT NULL DEFAULT 'exact',      -- exact / contain
  reply_content TEXT NOT NULL DEFAULT '',
  reply_type TEXT NOT NULL DEFAULT 'text',        -- text / image / voice
  is_enabled BOOLEAN NOT NULL DEFAULT true,
  priority INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

IV. Edge Functions Details

4.1 wechat-token — Token Management Service

Responsibility: Obtain and cache WeChat access_token. All other Edge Functions get tokens by calling this service. Request Method: GET or POST Core Logic:
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

// Support reverse proxy: use proxy if SUPERUN_WECHAT_API_PROXY_URL is configured, otherwise direct connection
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
const WECHAT_TOKEN_URL = `${WECHAT_API_BASE}/cgi-bin/token`;
const TOKEN_BUFFER_MINUTES = 5; // Refresh 5 minutes early

serve(async (req) => {
  const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
  const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
  const supabase = createClient(supabaseUrl, supabaseServiceKey);

  // 1. Check if there is a non-expired cached token in the database
  const bufferTime = new Date(
    Date.now() + TOKEN_BUFFER_MINUTES * 60 * 1000
  ).toISOString();
  const { data: cachedToken } = await supabase
    .from("wechat_tokens")
    .select("access_token, expires_at")
    .gt("expires_at", bufferTime)
    .order("created_at", { ascending: false })
    .limit(1)
    .maybeSingle();

  if (cachedToken) {
    return new Response(
      JSON.stringify({ access_token: cachedToken.access_token }),
      { status: 200 }
    );
  }

  // 2. Cache expired, request new token from WeChat API
  const appId = Deno.env.get("SUPERUN_WECHAT_APP_ID");
  const appSecret = Deno.env.get("SUPERUN_WECHAT_APP_SECRET");
  const tokenUrl =
    `${WECHAT_TOKEN_URL}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
  const wechatResponse = await fetch(tokenUrl);
  const wechatData = await wechatResponse.json();

  // 3. Handle IP whitelist error (errcode 40164)
  if (wechatData.errcode) {
    let blockedIp: string | null = null;
    if (wechatData.errcode === 40164 && wechatData.errmsg) {
      const ipMatch = wechatData.errmsg.match(/invalid ip (\d+\.\d+\.\d+\.\d+)/);
      if (ipMatch) blockedIp = ipMatch[1];
    }
    return new Response(
      JSON.stringify({
        error: `WeChat API error: ${wechatData.errmsg}`,
        errcode: wechatData.errcode,
        blocked_ip: blockedIp,
      }),
      { status: 502 }
    );
  }

  // 4. Cache new token to database
  const expiresAt = new Date(
    Date.now() + wechatData.expires_in * 1000
  ).toISOString();
  await supabase.from("wechat_tokens").insert({
    access_token: wechatData.access_token,
    expires_at: expiresAt,
  });

  // 5. Clean up expired old tokens
  await supabase
    .from("wechat_tokens")
    .delete()
    .lt("expires_at", new Date().toISOString());

  return new Response(
    JSON.stringify({ access_token: wechatData.access_token }),
    { status: 200 }
  );
});
Return Format:
// Success
{ "access_token": "ACCESS_TOKEN_VALUE" }

// Failure (IP whitelist issue)
{
  "error": "WeChat API error: invalid ip 1.2.3.4...",
  "errcode": 40164,
  "blocked_ip": "1.2.3.4"
}

4.2 wechat-data — Data Statistics Service

Responsibility: Proxy WeChat data statistics interfaces (datacube) and follower / menu queries. Request Method: POST TokenResult Pattern (unified use for all functions requiring access_token):
interface TokenResult {
  token?: string;
  error?: string;
  blocked_ip?: string | null;
}

async function getAccessToken(): Promise<TokenResult> {
  const response = await fetch(
    `${SUPABASE_URL}/functions/v1/wechat-token`,
    {
      headers: {
        Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
      },
    }
  );
  const data = await response.json();
  if (!data.access_token) {
    return {
      error: data.error || "Failed to get access token",
      blocked_ip: data.blocked_ip || null,
    };
  }
  return { token: data.access_token };
}
Key Design: When token acquisition fails (e.g., IP blocked), do not throw an exception. Instead, return { blocked_ip } to the frontend for friendly display.
Supported actions:
actionWeChat APIDescriptionDate Limit
get-menuGET /cgi-bin/menu/getGet current menuNone
get-follower-countGET /cgi-bin/user/getGet total followersNone
user-summaryPOST /datacube/getusersummaryUser growth dataMax 7 days
user-cumulatePOST /datacube/getusercumulateCumulative user dataMax 7 days
article-summaryPOST /datacube/getarticlesummaryArticle broadcast daily dataMax 1 day
article-totalPOST /datacube/getarticletotalArticle broadcast total dataMax 1 day
upstream-msgPOST /datacube/getupstreammsgMessage overviewMax 7 days
interface-summaryPOST /datacube/getinterfacesummaryInterface analysisMax 30 days
Call Example (Frontend → Edge Function):
import { supabase } from "@/integrations/supabase/client";

// Get follower count
const { data } = await supabase.functions.invoke("wechat-data", {
  body: { action: "get-follower-count" },
});
console.log(data.data.total); // Total followers

// Get user growth data (default last 7 days)
const { data: userData } = await supabase.functions.invoke("wechat-data", {
  body: {
    action: "user-summary",
    begin_date: "2026-02-26", // Optional, format YYYY-MM-DD
    end_date: "2026-03-04",   // Optional
  },
});
console.log(userData.data); // [{ ref_date, user_source, ... }]

4.3 wechat-articles — Article Management Service

Responsibility: Article CRUD, publish to WeChat, sync published articles from WeChat. Supported actions:
actionDescriptionRequires Authentication
save-draftSave / update draft
publishPublish to WeChat (draft → broadcast)
deleteSoft delete article
sync-publishedSync published articles from WeChat
Publishing Process (two-step operation):
// All functions start with this line to support proxy switching
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
// 1. First add to WeChat draft box
const draftResponse = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/draft/add?access_token=${accessToken}`,
  {
    method: "POST",
    body: JSON.stringify({
      articles: [{
        title: "Article Title",
        author: "Author",
        digest: "Summary",
        content: "<p>Article HTML</p>",
        content_source_url: "",
        thumb_media_id: "",       // Cover material ID (can be empty)
        need_open_comment: 0,
        only_fans_can_comment: 0,
      }],
    }),
  }
);
const { media_id } = await draftResponse.json();

// 2. Submit for broadcast
const publishResponse = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/freepublish/submit?access_token=${accessToken}`,
  {
    method: "POST",
    body: JSON.stringify({ media_id }),
  }
);
const { publish_id } = await publishResponse.json();
Sync Published Articles (core logic):
// ⚠️ Key point: freepublish/batchget returns article_id, not media_id!
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
const batchResponse = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/freepublish/batchget?access_token=${accessToken}`,
  {
    method: "POST",
    body: JSON.stringify({ offset: 0, count: 20, no_content: 0 }),
  }
);
const batchResult = await batchResponse.json();

// Return structure example:
// {
//   "total_count": 5,
//   "item_count": 5,
//   "item": [
//     {
//       "article_id": "Ai6E...",    ← Correct field name
//       "content": {
//         "news_item": [
//           { "title": "...", "author": "...", "url": "...", ... }
//         ]
//       },
//       "update_time": 1709712000
//     }
//   ]
// }

// Deduplication logic: use article_id to match wechat_media_id field in database
const articleIds = allItems.map((item) => item.article_id);
const { data: existing } = await supabaseAdmin
  .from("articles")
  .select("wechat_media_id")
  .in("wechat_media_id", articleIds);

// When inserting, use article_id + index to distinguish multi-article posts
const wechat_media_id = newsIndex === 0
  ? item.article_id
  : `${item.article_id}_${newsIndex}`;
Frontend Calls:
// Sync from WeChat
const { data } = await supabase.functions.invoke("wechat-articles", {
  body: { action: "sync-published" },
});
// data: { success: true, new_count: 5, total_count: 5 }

// Save draft
const { data } = await supabase.functions.invoke("wechat-articles", {
  body: {
    action: "save-draft",
    title: "Title",
    content: "<p>Content</p>",
    author: "Author",
    digest: "Summary",
  },
});

// Publish to WeChat
const { data } = await supabase.functions.invoke("wechat-articles", {
  body: { action: "publish", article_id: "uuid-of-article" },
});

4.4 wechat-menu — Custom Menu Service

Responsibility: Sync local menu configuration to WeChat, clear WeChat menu. Supported actions:
actionDescriptionParameters
publishPublish menu to WeChatmenu_config_id (required)
deleteClear WeChat menumenu_config_id (optional)
Core Logic:
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
// Publish menu
const response = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/menu/create?access_token=${accessToken}`,
  {
    method: "POST",
    body: JSON.stringify(menuData), // { button: [...] }
  }
);

// Clear menu
const response = await fetch(
  `${WECHAT_API_BASE}/cgi-bin/menu/delete?access_token=${accessToken}`,
  { method: "GET" }
);
Smart Handling: When publish finds an empty menu (button.length === 0), automatically call the delete interface to clear the WeChat menu instead of creating an empty menu. Menu Data Structure (WeChat standard format):
{
  "button": [
    {
      "type": "view",
      "name": "Official Website",
      "url": "https://example.com"
    },
    {
      "name": "Services",
      "sub_button": [
        {
          "type": "view",
          "name": "Online Support",
          "url": "https://example.com/service"
        },
        {
          "type": "click",
          "name": "Latest Activities",
          "key": "LATEST_ACTIVITY"
        }
      ]
    }
  ]
}

V. Frontend Integration Patterns

5.1 Unified Hook Encapsulation

All WeChat API calls are encapsulated as React Query hooks:
// hooks/useArticles.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";

interface SyncResult {
  success?: boolean;
  new_count?: number;
  total_count?: number;
  error?: string;
  blocked_ip?: string | null;
}

export function useSyncWechatArticles() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: async (): Promise<SyncResult> => {
      const { data, error } = await supabase.functions.invoke(
        "wechat-articles",
        { body: { action: "sync-published" } }
      );
      if (error) throw error;
      if (data?.blocked_ip) {
        return { blocked_ip: data.blocked_ip, new_count: 0 };
      }
      if (data?.error) throw new Error(data.error);
      return data as SyncResult;
    },
    onSuccess: (result) => {
      if (!result.blocked_ip) {
        queryClient.invalidateQueries({ queryKey: ["articles"] });
      }
    },
  });
}

5.2 IP Whitelist Error Handling (Common Pattern)

All pages interacting with WeChat implement unified IP whitelist error handling:
const [lastBlockedIp, setLastBlockedIp] = useState<string | null>(null);

// Handle response
const handleResult = (result: { blocked_ip?: string | null }) => {
  if (result.blocked_ip) {
    setLastBlockedIp(result.blocked_ip);
    toast.error(
      `WeChat API connection failed. Please add IP ${result.blocked_ip} to the Official Account IP whitelist`,
      { duration: 15000 }
    );
    return;
  }
  // ... Normal processing
};

// UI alert bar
{lastBlockedIp && (
  <div className="bg-orange-50 border border-orange-200 rounded-lg p-3">
    <p>
      IP <code>{lastBlockedIp}</code> is not in the WeChat Official Account whitelist
    </p>
    <button onClick={() => navigator.clipboard.writeText(lastBlockedIp)}>
      Copy IP
    </button>
  </div>
)}

5.3 Manual Sync (Connectivity Check)

// hooks/useWechatSync.ts
export function useWechatManualSync() {
  const queryClient = useQueryClient();
  const [isSyncing, setIsSyncing] = useState(false);
  const [lastBlockedIp, setLastBlockedIp] = useState<string | null>(null);

  const syncWechatData = useCallback(async () => {
    setIsSyncing(true);
    setLastBlockedIp(null);

    // Use lightweight get-follower-count as connectivity check
    const { data, error } = await supabase.functions.invoke("wechat-data", {
      body: { action: "get-follower-count" },
    });

    setIsSyncing(false);

    if (data?.blocked_ip) {
      setLastBlockedIp(data.blocked_ip);
      return { success: false, blockedIp: data.blocked_ip };
    }
    if (error || data?.error) {
      return { success: false };
    }

    // Refresh related data on success
    queryClient.invalidateQueries({ queryKey: ["wechat-overview"] });
    return { success: true };
  }, [queryClient]);

  return { syncWechatData, isSyncing, lastBlockedIp };
}

VI. Key Considerations

6.1 IP Whitelist Problem — Two Solutions

Supabase Edge Functions use dynamic egress IPs and cannot pre-configure whitelists. Users can choose either of the following solutions based on their situation:

Scheme A: IP Whitelist (Manual Maintenance)

Suitable for: Quick verification, temporary use, no server resources. Steps:
  1. First WeChat API call will return errcode: 40164
  2. Extract the rejected IP from errmsg: invalid ip x.x.x.x
  3. Add this IP to the Official Account whitelist
  4. The application frontend will display an orange alert bar with one-click IP copy
Disadvantages:
  • Edge Function IP may change, requiring re-addition
  • Requires manual operation in WeChat backend
Configuration: No additional configuration needed, this is the default mode.
Suitable for: Production environment, long-term use, don’t want to manually add whitelist every time IP changes. Principle: Deploy an Nginx reverse proxy on a server with a fixed public IP. All WeChat API requests are forwarded through this server. WeChat only sees the proxy server’s fixed IP, so adding it to the whitelist once makes it permanent. Step 1: Deploy Proxy Server Prepare a server with a fixed public IP (cloud server is fine, lowest configuration works), install Nginx, configure as follows:
server {
    listen 443 ssl;
    server_name wechat-proxy.your-domain.com;
    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    location / {
        proxy_pass https://api.weixin.qq.com;
        proxy_set_header Host api.weixin.qq.com;
        proxy_ssl_server_name on;
    }
}
HTTP (port 80) can also be used, but HTTPS is recommended for transmission security.
Step 2: Configure WeChat IP Whitelist Add the proxy server’s fixed public IP to the WeChat Developer Platform “Basic Information → Developer Secret → API IP Whitelist”. Step 3: Configure Edge Function Secret Add Edge Function Secret in Supabase backend:
Secret NameValueExample
SUPERUN_WECHAT_API_PROXY_URLProxy server addresshttps://wechat-proxy.your-domain.com
Note: Do not add / at the end of the address.
Step 4: Verify After configuration, redeploy Edge Functions and call any WeChat interface to verify connectivity. Code-Level Implementation: All Edge Functions automatically switch between direct connection / proxy mode through this single line of code:
const WECHAT_API_BASE = Deno.env.get("SUPERUN_WECHAT_API_PROXY_URL") || "https://api.weixin.qq.com";
  • SUPERUN_WECHAT_API_PROXY_URL not configured → Direct connection to api.weixin.qq.com (Scheme A)
  • Configured → All requests go through proxy address (Scheme B)
The following 4 Edge Functions all support this configuration:
  • wechat-token (1 location)
  • wechat-data (8 locations)
  • wechat-articles (3 locations)
  • wechat-menu (3 locations)
Switching Method:
  • Switch from Scheme A to Scheme B: Add SUPERUN_WECHAT_API_PROXY_URL Secret and redeploy
  • Switch from Scheme B back to Scheme A: Delete the Secret and redeploy
  • No code changes needed for switching

Comparison of Two Schemes

Scheme A: IP WhitelistScheme B: Reverse Proxy
Configuration DifficultyNo additional server neededRequires a cloud server + domain
Maintenance CostManual update when IP changesNo maintenance after one-time setup
StabilityDepends on Edge Function IP staying unchangedAlways stable
Suitable ForDevelopment testing, temporary useProduction environment, long-term operation
Switching MethodDefaultAdd/delete Secret

6.2 Access Token Management

  • WeChat access_token is valid for 2 hours with limited daily calls
  • Cached in wechat_tokens table, refreshed 5 minutes early
  • All Edge Functions get tokens by calling wechat-token function to avoid duplicate requests
  • Regularly clean up expired token records

6.3 Common WeChat API errcode

errcodeMeaningHandling Method
40001access_token invalidClear cache and re-acquire
40164IP not in whitelistExtract IP and prompt user to add
42001access_token expiredAuto refresh
46003Menu does not existNormal case, return empty menu
61501Date range exceeds limitCheck if date range exceeds API limit
45047Exceeds publish frequency limitPrompt user to retry later

6.4 Data Statistics Date Limits

  • WeChat data has 1 day delay, end_date can only be as early as yesterday
  • getusersummary / getusercumulate: Maximum span 7 days (inclusive)
  • getarticlesummary / getarticletotal: Maximum span 1 day
  • getupstreammsg: Maximum span 7 days
  • getinterfacesummary: Maximum span 30 days
  • Date format: YYYY-MM-DD

6.5 freepublish/batchget Field Notes

  • The identifier field in each record returned by this interface is article_id, not media_id
  • media_id is the field returned by the draft box interface (draft/add)
  • Confusing these two fields will cause all values to be undefined during sync, preventing data from being written

6.6 Inter-Edge Function Calls

  • wechat-data, wechat-articles, wechat-menu all call wechat-token via HTTP
  • Use SUPABASE_ANON_KEY or SUPABASE_SERVICE_ROLE_KEY as Bearer Token when calling
  • Set verify_jwt = false in config.toml to allow cross-function calls

6.7 Error Return Strategy

All Edge Functions involving WeChat API uniformly follow:
  • Token acquisition failure + IP blocked: Return HTTP 200 + { blocked_ip: "x.x.x.x" }
    • Returning 200 allows frontend supabase.functions.invoke() to correctly read data (non-200 will be put into error)
  • WeChat API business error: Return HTTP 200 + { error: "...", errcode: xxx }
  • System error: Return HTTP 500 + { error: "..." }

VII. Deployment Checklist

Pre-Deployment Checklist

Common Steps (both schemes require):
  • Obtained AppID and AppSecret from WeChat Developer Platform
  • Configured SUPERUN_WECHAT_APP_ID and SUPERUN_WECHAT_APP_SECRET in Supabase Edge Function Secrets
  • Created database tables: wechat_tokens, articles, menu_configs, auto_reply_rules
  • Set verify_jwt = false for all WeChat-related functions in supabase/config.toml
Scheme A Additional Steps (IP Whitelist):
  • After deployment, get Edge Function egress IP from error response on first call
  • Add egress IP to WeChat Developer Platform IP whitelist
  • Call again to verify connectivity
Scheme B Additional Steps (Reverse Proxy):
  • Deploy proxy server (Nginx configuration see section 6.1)
  • Add proxy server’s fixed IP to WeChat IP whitelist
  • Configure Edge Function Secret SUPERUN_WECHAT_API_PROXY_URL
  • Redeploy and verify connectivity

Edge Function File Structure

supabase/functions/
├── wechat-token/
│   └── index.ts       # Token management (get, cache, cleanup)
├── wechat-data/
│   └── index.ts       # Data statistics proxy (followers, menu, datacube)
├── wechat-articles/
│   └── index.ts       # Article management (CRUD, publish, sync)
├── wechat-menu/
│   └── index.ts       # Menu management (publish, clear)
└── ai-content-assist/
    └── index.ts       # AI content assistance (not WeChat related)