Edge Functions
Supabase Edge Functions for plugin publishing, metrics, and install tracking.
Cursorist uses Supabase Edge Functions for server-side operations that need privileged database access — publishing plugins to GitHub, tracking installs, and gathering admin metrics. They run on Deno at the edge and share a common set of utilities for auth, CORS, and response formatting.
Structure
supabase/functions/
├── _shared/
│ ├── auth.ts # Admin authentication helper
│ ├── cors.ts # CORS headers + json/error response helpers
│ ├── github.ts # GitHub API (create/delete files in repo)
│ └── supabase.ts # Supabase admin client (service role)
├── publish-rule/ # Publish a plugin to GitHub
├── unpublish-rule/ # Remove a plugin from GitHub
├── increment-install/ # Bump plugin install count
└── admin-metrics/ # Platform-wide statsShared Utilities
_shared/supabase.ts
Creates a single Supabase client with the service role key. This bypasses RLS so functions can read/write any table.
import { createClient } from "@supabase/supabase-js";
export const supabaseAdmin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);_shared/auth.ts
Extracts the admin check that all protected functions need. Returns the user ID on success or an error Response on failure.
import { supabaseAdmin } from "./supabase.ts";
import { error } from "./cors.ts";
export async function requireAdmin(req: Request): Promise<\{ userId: string \} | Response> \{
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) return error("Unauthorized", 401);
const \{ data: \{ user \} \} = await supabaseAdmin.auth.getUser(token);
if (!user) return error("Invalid token", 401);
const \{ data: profile \} = await supabaseAdmin
.from("users").select("is_admin").eq("id", user.id).single();
if (!profile?.is_admin) return error("Admin access required", 403);
return \{ userId: user.id \};
\}_shared/cors.ts
CORS headers plus json() and error() helpers that every function uses, so you never repeat the Response boilerplate.
_shared/github.ts
Wraps the GitHub Contents API to create, update, and delete files in the cursorist repository. Used by publish-rule and unpublish-rule to push plugin JSON to the repo.
Functions
publish-rule
Auth: Admin only | Method: POST
Publishes a plugin to GitHub by:
- Verifying admin auth via
requireAdmin() - Fetching the plugin, its latest stable version, and assets
- Building a JSON payload and pushing it to the repo via GitHub API
- Marking the plugin as
is_published = truein the database
Request body:
\{ "plugin_id": "uuid" \}unpublish-rule
Auth: Admin only | Method: POST
Reverses a publish by deleting the plugin JSON from GitHub and setting is_published = false.
Request body:
\{ "plugin_id": "uuid" \}increment-install
Auth: None (public) | Method: POST
Calls the increment_plugin_install_count database function to atomically bump a plugin's install count. Called from the frontend when a user installs a plugin.
Request body:
\{ "plugin_id": "uuid" \}admin-metrics
Auth: Admin only | Method: GET
Returns platform-wide metrics in a single response:
\{
"users": \{ "total": 42, "new_last_7_days": 5 \},
"organizations": \{ "total": 8 \},
"teams": \{ "total": 15 \},
"plugins": \{ "total": 23, "published": 18, "top": [...] \}
\}Deployment
Deploy all functions at once:
supabase functions deploy publish-rule
supabase functions deploy unpublish-rule
supabase functions deploy increment-install
supabase functions deploy admin-metricsSet the GitHub token secret (needed for publish/unpublish):
supabase secrets set GITHUB_TOKEN=ghp_your_token_hereCalling from the Frontend
import { createClient } from "@/lib/supabase/client";
const supabase = createClient();
await supabase.functions.invoke("publish-rule", \{
body: \{ plugin_id: "..." \},
\});
await supabase.functions.invoke("increment-install", \{
body: \{ plugin_id: "..." \},
\});
const \{ data \} = await supabase.functions.invoke("admin-metrics");