diff --git a/.icons/supabase.svg b/.icons/supabase.svg
new file mode 100644
index 000000000..9e31b162a
--- /dev/null
+++ b/.icons/supabase.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/registry/coder/modules/supabase/README.md b/registry/coder/modules/supabase/README.md
new file mode 100644
index 000000000..75b365b21
--- /dev/null
+++ b/registry/coder/modules/supabase/README.md
@@ -0,0 +1,200 @@
+---
+display_name: Supabase CLI
+description: Install Supabase CLI and configure authentication via Coder external auth or access token
+icon: ../../../../.icons/supabase.svg
+verified: false
+tags: [supabase, database, cli, helper]
+---
+
+# Supabase CLI
+
+This module adds the [Supabase CLI](https://supabase.com/docs/guides/cli) to your Coder workspace with pre-configured authentication. Instead of manually installing the CLI and running `supabase login` in each workspace session, the module handles installation and injects credentials via environment variables—so `supabase projects list` and other commands work immediately.
+
+It integrates with Coder's external auth for OAuth-based login, or accepts a personal access token for simpler setups. When a `project_ref` is provided, the module also links the workspace to your Supabase project and adds a dashboard shortcut to the Coder workspace UI.
+
+**What this module does:** Installs the Supabase CLI in your workspace and wires up authentication through Coder's [external auth](https://coder.com/docs/admin/external-auth) (OAuth) or a personal access token. Once configured, users get a ready-to-use `supabase` command—no manual login required—plus a dashboard button in the workspace UI.
+
+```tf
+module "supabase" {
+ source = "registry.coder.com/coder/supabase/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+}
+```
+
+## Authentication
+
+Choose **one** of the following authentication methods:
+
+### Option 1: Personal Access Token
+
+Generate a token at [supabase.com/dashboard/account/tokens](https://supabase.com/dashboard/account/tokens) and pass it to the module via the `access_token` variable with `use_external_auth = false`.
+
+### Option 2: Coder External Auth (OAuth)
+
+Configure Supabase as an [external auth provider](https://coder.com/docs/admin/external-auth) in your Coder deployment. Users authenticate via OAuth when launching a workspace.
+
+Required Coder environment variables:
+
+```bash
+CODER_EXTERNAL_AUTH_0_ID=supabase
+CODER_EXTERNAL_AUTH_0_TYPE=custom
+CODER_EXTERNAL_AUTH_0_CLIENT_ID=
+CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=
+CODER_EXTERNAL_AUTH_0_AUTH_URL=https://api.supabase.com/v1/oauth/authorize
+CODER_EXTERNAL_AUTH_0_TOKEN_URL=https://api.supabase.com/v1/oauth/token
+CODER_EXTERNAL_AUTH_0_SCOPES=all
+CODER_EXTERNAL_AUTH_0_DISPLAY_NAME=Supabase
+CODER_EXTERNAL_AUTH_0_DISPLAY_ICON=/icon/supabase.svg
+```
+
+Create your OAuth app in the [Supabase Dashboard](https://supabase.com/dashboard/account/oauth-apps) under "OAuth Apps" → "Published apps". Set the redirect URI to `https:///external-auth/supabase/callback`.
+
+## Usage
+
+### With Personal Access Token
+
+```tf
+variable "supabase_token" {
+ type = string
+ sensitive = true
+}
+
+module "supabase" {
+ source = "registry.coder.com/coder/supabase/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+ use_external_auth = false
+ access_token = var.supabase_token
+}
+```
+
+> [!NOTE]
+> Never hardcode tokens in your template.
+
+### With External Auth (OAuth)
+
+```tf
+module "supabase" {
+ source = "registry.coder.com/coder/supabase/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+ use_external_auth = true
+ # external_auth_id = "supabase" # Default; change if your provider has a different ID
+}
+```
+
+### With Project Dashboard Link
+
+```tf
+module "supabase" {
+ source = "registry.coder.com/coder/supabase/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+ use_external_auth = false
+ access_token = var.supabase_token
+ project_ref = "abcdefghijklmnop" # Links dashboard button directly to this project
+}
+```
+
+### With Custom Install Method
+
+```tf
+module "supabase" {
+ source = "registry.coder.com/coder/supabase/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+ install_method = "binary" # Force binary install instead of detect
+}
+```
+
+### Pre-installed Binary (Air-gapped / Golden Image)
+
+```tf
+module "supabase" {
+ source = "registry.coder.com/coder/supabase/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+ skip_install = true # CLI is already in the image
+ access_token = var.supabase_token
+}
+```
+
+### With Internal Mirror
+
+```tf
+module "supabase" {
+ source = "registry.coder.com/coder/supabase/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+ download_base_url = "https://artifacts.internal.corp/supabase-cli/releases/download"
+}
+```
+
+## Installation Methods
+
+The module supports multiple installation methods to work across different workspace environments:
+
+| Method | Description | Platforms |
+| ------------------ | ---------------------------- | -------------- |
+| `detect` (default) | Detect best available method | All |
+| `brew` | Homebrew | macOS, Linux |
+| `scoop` | Scoop package manager | Windows |
+| `binary` | Direct binary download | All (fallback) |
+
+Detection priority: Homebrew → Scoop → Native packages (deb/rpm/apk) → Binary
+
+## Dashboard App
+
+The module adds a **Supabase** button to your workspace that links to the Supabase dashboard:
+
+- **Without `project_ref`**: Links to [supabase.com/dashboard](https://supabase.com/dashboard) (project list)
+- **With `project_ref`**: Links directly to your project's dashboard
+
+Find your project reference in the Supabase dashboard URL: `https://supabase.com/dashboard/project/`
+
+## Common CLI Commands
+
+After workspace start, you can use the Supabase CLI:
+
+```bash
+# List your projects
+supabase projects list
+
+# Link to a project
+supabase link --project-ref
+
+# Database operations
+supabase db pull # Pull remote schema
+supabase db push # Push migrations
+supabase migration new # Create migration
+
+# Local development (requires Docker)
+supabase start # Start local stack
+supabase stop # Stop local stack
+
+# Generate TypeScript types
+supabase gen types typescript --project-id > types.ts
+```
+
+## Network Egress
+
+During installation and operation, the module and CLI may connect to these external endpoints:
+
+| Endpoint | Purpose | When |
+| -------------------- | ---------------------------------------- | --------------------------------- |
+| `api.github.com` | Resolve latest CLI version | Install (when version = "latest") |
+| `github.com` | Download CLI binary/package | Install |
+| `api.supabase.com` | OAuth authentication, project operations | Runtime (CLI commands) |
+| `supabase.com` | Dashboard links | Workspace app (external link) |
+| Homebrew/Scoop repos | Package installation | Install (brew/scoop methods) |
+
+To use in restricted environments, set `download_base_url` to an internal mirror or use `skip_install = true` with a pre-baked image.
+
+## Logs
+
+Installation logs are stored at:
+
+```
+$HOME/.coder-modules/coder/supabase/logs/install.log
+```
diff --git a/registry/coder/modules/supabase/main.test.ts b/registry/coder/modules/supabase/main.test.ts
new file mode 100644
index 000000000..025566a0b
--- /dev/null
+++ b/registry/coder/modules/supabase/main.test.ts
@@ -0,0 +1,286 @@
+import {
+ describe,
+ expect,
+ it,
+ beforeAll,
+ afterEach,
+ setDefaultTimeout,
+} from "bun:test";
+import {
+ execContainer,
+ removeContainer,
+ runContainer,
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+} from "~test";
+
+setDefaultTimeout(3 * 60 * 1000); // 3 minutes for CLI downloads
+
+let cleanupContainers: string[] = [];
+
+afterEach(async () => {
+ for (const id of cleanupContainers) {
+ try {
+ await removeContainer(id);
+ } catch {
+ // ignore cleanup errors
+ }
+ }
+ cleanupContainers = [];
+});
+
+describe("supabase", () => {
+ beforeAll(async () => {
+ await runTerraformInit(import.meta.dir);
+ });
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "test-agent",
+ });
+
+ it("missing variable: agent_id", async () => {
+ await expect(runTerraformApply(import.meta.dir, {})).rejects.toThrow(
+ /agent_id/,
+ );
+ });
+
+ it("defaults to detect install method", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ });
+ const script = state.resources.find(
+ (r) => r.type === "coder_script" && r.name === "install_script",
+ );
+ expect(script).toBeDefined();
+ // coder-utils wraps our script in base64, decode to check
+ const wrapperScript = script!.instances[0].attributes.script as string;
+ const b64Match = wrapperScript.match(/echo -n '([A-Za-z0-9+/=]+)'/);
+ expect(b64Match).toBeTruthy();
+ const decodedScript = Buffer.from(b64Match![1], "base64").toString("utf-8");
+ expect(decodedScript).toContain("INSTALL_METHOD='detect'");
+ });
+
+ it("accepts binary install method", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ install_method: "binary",
+ });
+ const script = state.resources.find(
+ (r) => r.type === "coder_script" && r.name === "install_script",
+ );
+ const wrapperScript = script!.instances[0].attributes.script as string;
+ const b64Match = wrapperScript.match(/echo -n '([A-Za-z0-9+/=]+)'/);
+ const decodedScript = Buffer.from(b64Match![1], "base64").toString("utf-8");
+ expect(decodedScript).toContain("INSTALL_METHOD='binary'");
+ });
+
+ it("accepts brew install method", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ install_method: "brew",
+ });
+ const script = state.resources.find(
+ (r) => r.type === "coder_script" && r.name === "install_script",
+ );
+ const wrapperScript = script!.instances[0].attributes.script as string;
+ const b64Match = wrapperScript.match(/echo -n '([A-Za-z0-9+/=]+)'/);
+ const decodedScript = Buffer.from(b64Match![1], "base64").toString("utf-8");
+ expect(decodedScript).toContain("INSTALL_METHOD='brew'");
+ });
+
+ it("accepts scoop install method", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ install_method: "scoop",
+ });
+ const script = state.resources.find(
+ (r) => r.type === "coder_script" && r.name === "install_script",
+ );
+ const wrapperScript = script!.instances[0].attributes.script as string;
+ const b64Match = wrapperScript.match(/echo -n '([A-Za-z0-9+/=]+)'/);
+ const decodedScript = Buffer.from(b64Match![1], "base64").toString("utf-8");
+ expect(decodedScript).toContain("INSTALL_METHOD='scoop'");
+ });
+
+ it("rejects invalid install method", async () => {
+ await expect(
+ runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ install_method: "invalid",
+ }),
+ ).rejects.toThrow(/install_method.*must be/);
+ });
+
+ it("supports skip_install option", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ skip_install: "true",
+ });
+ const script = state.resources.find(
+ (r) => r.type === "coder_script" && r.name === "install_script",
+ );
+ expect(script).toBeDefined();
+ const wrapperScript = script!.instances[0].attributes.script as string;
+ const b64Match = wrapperScript.match(/echo -n '([A-Za-z0-9+/=]+)'/);
+ expect(b64Match).toBeTruthy();
+ const decodedScript = Buffer.from(b64Match![1], "base64").toString("utf-8");
+ expect(decodedScript).toContain("Skipping Supabase CLI installation");
+ });
+
+ it("supports custom download_base_url", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ download_base_url: "https://mirror.internal/supabase",
+ });
+ const script = state.resources.find(
+ (r) => r.type === "coder_script" && r.name === "install_script",
+ );
+ const wrapperScript = script!.instances[0].attributes.script as string;
+ const b64Match = wrapperScript.match(/echo -n '([A-Za-z0-9+/=]+)'/);
+ const decodedScript = Buffer.from(b64Match![1], "base64").toString("utf-8");
+ expect(decodedScript).toContain("https://mirror.internal/supabase");
+ });
+
+ it("sets access_token when use_external_auth is false", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ use_external_auth: "false",
+ access_token: "sbp_test_token_abc123",
+ });
+ expect(state.outputs.access_token.value).toBe("sbp_test_token_abc123");
+ });
+
+ it("installs via binary on ubuntu", async () => {
+ const id = await runContainer("ubuntu:22.04");
+ cleanupContainers.push(id);
+
+ await execContainer(id, ["apt-get", "update"]);
+ await execContainer(id, ["apt-get", "install", "-y", "curl", "tar"]);
+
+ // Use pinned version to avoid GitHub API rate limits in CI (403 on /releases/latest)
+ const script = [
+ "#!/bin/bash",
+ "set -ex",
+ "export HOME=/root",
+ "BIN_DIR=$HOME/.coder-modules/coder/supabase/bin",
+ "mkdir -p $BIN_DIR",
+ "ARCH=$(uname -m)",
+ "case $ARCH in x86_64|amd64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; esac",
+ "OS=$(uname -s | tr A-Z a-z)",
+ "VERSION=2.22.12",
+ "DOWNLOAD_URL=https://github.com/supabase/cli/releases/download/v$VERSION/supabase_${OS}_${ARCH}.tar.gz",
+ "curl -fsSL -o /tmp/supabase.tar.gz $DOWNLOAD_URL",
+ "tar -xzf /tmp/supabase.tar.gz -C /tmp",
+ "mv /tmp/supabase $BIN_DIR/supabase",
+ "chmod +x $BIN_DIR/supabase",
+ "$BIN_DIR/supabase --version",
+ ].join("\n");
+
+ await execContainer(id, [
+ "sh",
+ "-c",
+ "cat > /tmp/install.sh << 'SCRIPT'\n" + script + "\nSCRIPT",
+ ]);
+ await execContainer(id, ["chmod", "+x", "/tmp/install.sh"]);
+ const result = await execContainer(id, ["/tmp/install.sh"]);
+
+ if (result.exitCode !== 0) {
+ console.error("STDOUT:", result.stdout);
+ console.error("STDERR:", result.stderr);
+ }
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout).toMatch(/\d+\.\d+\.\d+/); // version number like 2.115.0
+ });
+
+ // Note: Binary install on Alpine (musl libc) doesn't work because Supabase CLI
+ // is compiled for glibc. In production, the module would fall back to apk package.
+ // This test verifies binary install on a glibc-based distro (Debian).
+ it("installs via binary on debian", async () => {
+ const id = await runContainer("debian:bookworm-slim");
+ cleanupContainers.push(id);
+
+ await execContainer(id, ["apt-get", "update"]);
+ await execContainer(id, [
+ "apt-get",
+ "install",
+ "-y",
+ "curl",
+ "ca-certificates",
+ ]);
+
+ const script = [
+ "#!/bin/bash",
+ "set -ex",
+ "export HOME=/root",
+ "BIN_DIR=$HOME/.coder-modules/coder/supabase/bin",
+ "mkdir -p $BIN_DIR",
+ "ARCH=$(uname -m)",
+ "case $ARCH in x86_64|amd64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; esac",
+ "OS=$(uname -s | tr A-Z a-z)",
+ "VERSION=2.22.12",
+ "DOWNLOAD_URL=https://github.com/supabase/cli/releases/download/v$VERSION/supabase_${OS}_${ARCH}.tar.gz",
+ "curl -fsSL -o /tmp/supabase.tar.gz $DOWNLOAD_URL",
+ "tar -xzf /tmp/supabase.tar.gz -C /tmp",
+ "mv /tmp/supabase $BIN_DIR/supabase",
+ "chmod +x $BIN_DIR/supabase",
+ "$BIN_DIR/supabase --version",
+ ].join("\n");
+
+ await execContainer(id, [
+ "sh",
+ "-c",
+ "cat > /tmp/install.sh << 'SCRIPT'\n" + script + "\nSCRIPT",
+ ]);
+ await execContainer(id, ["chmod", "+x", "/tmp/install.sh"]);
+ const result = await execContainer(id, ["/tmp/install.sh"]);
+
+ if (result.exitCode !== 0) {
+ console.error("Debian STDOUT:", result.stdout);
+ console.error("Debian STDERR:", result.stderr);
+ }
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout).toMatch(/\d+\.\d+\.\d+/); // version number like 2.115.0
+ });
+
+ it("creates CODER_SCRIPT_BIN_DIR symlink", async () => {
+ const id = await runContainer("ubuntu:22.04");
+ cleanupContainers.push(id);
+
+ await execContainer(id, ["apt-get", "update"]);
+ await execContainer(id, ["apt-get", "install", "-y", "curl", "tar"]);
+
+ const script = [
+ "#!/bin/bash",
+ "set -ex",
+ "export HOME=/root",
+ "export CODER_SCRIPT_BIN_DIR=/tmp/coder-bin",
+ "mkdir -p $CODER_SCRIPT_BIN_DIR",
+ "BIN_DIR=$HOME/.coder-modules/coder/supabase/bin",
+ "mkdir -p $BIN_DIR",
+ "ARCH=$(uname -m)",
+ "case $ARCH in x86_64|amd64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; esac",
+ "OS=$(uname -s | tr A-Z a-z)",
+ "VERSION=2.22.12",
+ "DOWNLOAD_URL=https://github.com/supabase/cli/releases/download/v$VERSION/supabase_${OS}_${ARCH}.tar.gz",
+ "curl -fsSL -o /tmp/supabase.tar.gz $DOWNLOAD_URL",
+ "tar -xzf /tmp/supabase.tar.gz -C /tmp",
+ "mv /tmp/supabase $BIN_DIR/supabase",
+ "chmod +x $BIN_DIR/supabase",
+ "ln -sf $BIN_DIR/supabase $CODER_SCRIPT_BIN_DIR/supabase",
+ "ls -la $CODER_SCRIPT_BIN_DIR/supabase",
+ "$CODER_SCRIPT_BIN_DIR/supabase --version",
+ ].join("\n");
+
+ await execContainer(id, [
+ "sh",
+ "-c",
+ "cat > /tmp/install.sh << 'SCRIPT'\n" + script + "\nSCRIPT",
+ ]);
+ await execContainer(id, ["chmod", "+x", "/tmp/install.sh"]);
+ const result = await execContainer(id, ["/tmp/install.sh"]);
+
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout).toMatch(/\d+\.\d+\.\d+/); // version number like 2.115.0
+ });
+});
diff --git a/registry/coder/modules/supabase/main.tf b/registry/coder/modules/supabase/main.tf
new file mode 100644
index 000000000..b43c0e7ad
--- /dev/null
+++ b/registry/coder/modules/supabase/main.tf
@@ -0,0 +1,184 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.0"
+ }
+ }
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+variable "icon" {
+ type = string
+ description = "The icon to use for the Supabase app."
+ default = "/icon/supabase.svg"
+}
+
+variable "external_auth_id" {
+ type = string
+ description = "Supabase external auth provider ID configured in Coder."
+ default = "supabase"
+}
+
+variable "use_external_auth" {
+ type = bool
+ description = "Use Coder external auth for Supabase authentication. Note: The Supabase CLI may reject OAuth tokens due to format validation; if so, set to false and use a Personal Access Token via the access_token variable."
+ default = false
+}
+
+variable "access_token" {
+ type = string
+ description = "Supabase personal access token. Only used when use_external_auth is false."
+ default = ""
+ sensitive = true
+}
+
+variable "install_method" {
+ type = string
+ description = "How to install the Supabase CLI. 'detect' automatically selects the best available method (brew → scoop → native package → binary). Use 'brew', 'scoop', or 'binary' to force a specific method."
+ default = "detect"
+ validation {
+ condition = contains(["detect", "brew", "scoop", "binary"], var.install_method)
+ error_message = "The 'install_method' variable must be one of: 'detect', 'brew', 'scoop', 'binary'."
+ }
+}
+
+variable "supabase_version" {
+ type = string
+ description = "The version of Supabase CLI to install. Use 'latest' for the most recent release."
+ default = "latest"
+}
+
+variable "download_base_url" {
+ type = string
+ description = "Base URL for downloading Supabase CLI releases. Override to use an internal mirror in restricted environments. The URL should serve the same directory structure as GitHub releases."
+ default = "https://github.com/supabase/cli/releases/download"
+}
+
+variable "skip_install" {
+ type = bool
+ description = "Skip CLI installation (use when supabase is already in the image). Auth environment variables are still configured."
+ default = false
+}
+
+variable "db_password" {
+ type = string
+ description = "Remote Postgres database password for non-interactive CLI commands like 'supabase link' (optional). Sets SUPABASE_DB_PASSWORD environment variable."
+ default = ""
+ sensitive = true
+}
+
+variable "project_ref" {
+ type = string
+ description = "Supabase project reference (e.g., 'abcdefghijklmnop'). When set, links the CLI to this project and adds a dashboard link."
+ default = ""
+}
+
+variable "project_dir" {
+ type = string
+ description = "Directory to link the Supabase project in. Created if it doesn't exist. Defaults to $HOME if empty."
+ default = ""
+}
+
+variable "dashboard_app" {
+ type = bool
+ description = "Whether to create the Supabase dashboard workspace app."
+ default = true
+}
+
+
+variable "pre_install_script" {
+ type = string
+ description = "Custom script to run before installing Supabase CLI. Can be used for dependency ordering between modules."
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Custom script to run after installing Supabase CLI."
+ default = null
+}
+
+# External auth data source - only used when use_external_auth is true
+data "coder_external_auth" "supabase" {
+ count = var.use_external_auth ? 1 : 0
+ id = var.external_auth_id
+}
+
+locals {
+ module_dir_name = ".coder-modules/coder/supabase"
+
+ # Determine the access token to use
+ access_token = var.use_external_auth ? try(data.coder_external_auth.supabase[0].access_token, "") : var.access_token
+
+ # Render the install script
+ install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
+ ARG_SKIP_INSTALL = tostring(var.skip_install)
+ ARG_INSTALL_METHOD = var.install_method
+ ARG_VERSION = var.supabase_version
+ ARG_PROJECT_REF = var.project_ref
+ ARG_PROJECT_DIR = var.project_dir
+ ARG_DOWNLOAD_BASE_URL = var.download_base_url
+ ARG_ACCESS_TOKEN = base64encode(local.access_token)
+ })
+}
+
+module "coder_utils" {
+ source = "registry.coder.com/coder/coder-utils/coder"
+ version = "0.0.1"
+
+ agent_id = var.agent_id
+ module_directory = "$HOME/${local.module_dir_name}"
+ display_name_prefix = "Supabase"
+ icon = var.icon
+ pre_install_script = var.pre_install_script
+ install_script = local.install_script
+ post_install_script = var.post_install_script
+}
+
+resource "coder_env" "supabase_db_password" {
+ count = var.db_password != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "SUPABASE_DB_PASSWORD"
+ value = var.db_password
+}
+
+resource "coder_app" "supabase" {
+ count = var.dashboard_app ? 1 : 0
+ agent_id = var.agent_id
+ slug = "supabase"
+ display_name = "Supabase"
+ icon = var.icon
+ url = var.project_ref != "" ? "https://supabase.com/dashboard/project/${var.project_ref}" : "https://supabase.com/dashboard"
+ external = true
+}
+
+
+# Pass-through of coder-utils script outputs so upstream modules can serialize
+# their coder_script resources behind this module's install pipeline using
+# `coder exp sync want `.
+output "scripts" {
+ description = "Ordered list of coder exp sync names for the coder_script resources this module creates, in run order (pre_install, install, post_install). Scripts that were not configured are absent from the list."
+ value = module.coder_utils.scripts
+}
+
+output "access_token" {
+ description = "The Supabase access token (from external auth or direct variable)."
+ value = local.access_token
+ sensitive = true
+}
+
+output "module_directory" {
+ description = "The directory where Supabase CLI logs and scripts are stored."
+ value = "$HOME/${local.module_dir_name}"
+}
diff --git a/registry/coder/modules/supabase/main.tftest.hcl b/registry/coder/modules/supabase/main.tftest.hcl
new file mode 100644
index 000000000..7e84b59ae
--- /dev/null
+++ b/registry/coder/modules/supabase/main.tftest.hcl
@@ -0,0 +1,302 @@
+run "test_supabase_basic" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-123"
+ }
+
+ assert {
+ condition = var.agent_id == "test-agent-123"
+ error_message = "Agent ID variable should be set correctly"
+ }
+
+ assert {
+ condition = var.install_method == "detect"
+ error_message = "Install method should default to 'detect'"
+ }
+
+ assert {
+ condition = var.supabase_version == "latest"
+ error_message = "Version should default to 'latest'"
+ }
+
+ assert {
+ condition = var.use_external_auth == false
+ error_message = "use_external_auth should default to false (PAT method)"
+ }
+}
+
+run "test_supabase_with_direct_token" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-456"
+ use_external_auth = false
+ access_token = "sbp_test_token_1234567890abcdef12345678"
+ }
+
+ assert {
+ condition = var.use_external_auth == false
+ error_message = "use_external_auth should be false"
+ }
+
+ assert {
+ condition = var.access_token == "sbp_test_token_1234567890abcdef12345678"
+ error_message = "Access token should be set correctly"
+ }
+}
+
+run "test_supabase_with_custom_install_method" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-789"
+ install_method = "binary"
+ }
+
+ assert {
+ condition = var.install_method == "binary"
+ error_message = "Install method should be 'binary'"
+ }
+}
+
+run "test_supabase_with_brew_install" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-brew"
+ install_method = "brew"
+ }
+
+ assert {
+ condition = var.install_method == "brew"
+ error_message = "Install method should be 'brew'"
+ }
+}
+
+run "test_supabase_with_scoop_install" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-scoop"
+ install_method = "scoop"
+ }
+
+ assert {
+ condition = var.install_method == "scoop"
+ error_message = "Install method should be 'scoop'"
+ }
+}
+
+run "test_supabase_with_specific_version" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-version"
+ supabase_version = "2.0.0"
+ }
+
+ assert {
+ condition = var.supabase_version == "2.0.0"
+ error_message = "Version should be '2.0.0'"
+ }
+}
+
+run "test_supabase_with_db_password" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-db"
+ use_external_auth = false
+ access_token = "sbp_test_token"
+ db_password = "my-secret-password"
+ }
+
+ assert {
+ condition = var.db_password == "my-secret-password"
+ error_message = "Database password should be set correctly"
+ }
+}
+
+run "test_supabase_with_custom_external_auth_id" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-auth"
+ external_auth_id = "my-supabase-oauth"
+ }
+
+ assert {
+ condition = var.external_auth_id == "my-supabase-oauth"
+ error_message = "External auth ID should be 'my-supabase-oauth'"
+ }
+}
+
+run "test_supabase_with_custom_icon" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-icon"
+ icon = "/icon/custom-supabase.svg"
+ }
+
+ assert {
+ condition = var.icon == "/icon/custom-supabase.svg"
+ error_message = "Icon should be set to custom path"
+ }
+}
+
+run "test_supabase_with_pre_install_script" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-pre"
+ pre_install_script = "echo 'Pre-install script'"
+ }
+
+ assert {
+ condition = var.pre_install_script == "echo 'Pre-install script'"
+ error_message = "Pre-install script should be set"
+ }
+}
+
+run "test_supabase_with_post_install_script" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-post"
+ post_install_script = "echo 'Post-install script'"
+ }
+
+ assert {
+ condition = var.post_install_script == "echo 'Post-install script'"
+ error_message = "Post-install script should be set"
+ }
+}
+
+run "test_supabase_app_default_url" {
+ command = apply
+
+ variables {
+ agent_id = "test-agent-app"
+ }
+
+ assert {
+ condition = resource.coder_app.supabase[0].url == "https://supabase.com/dashboard"
+ error_message = "coder_app URL should default to dashboard when project_ref is empty"
+ }
+
+ assert {
+ condition = resource.coder_app.supabase[0].external == true
+ error_message = "coder_app should be external"
+ }
+
+ assert {
+ condition = resource.coder_app.supabase[0].slug == "supabase"
+ error_message = "coder_app slug should be 'supabase'"
+ }
+}
+
+run "test_supabase_app_with_project_ref" {
+ command = apply
+
+ variables {
+ agent_id = "test-agent-project"
+ project_ref = "abcdefghijklmnop"
+ }
+
+ assert {
+ condition = var.project_ref == "abcdefghijklmnop"
+ error_message = "project_ref should be set correctly"
+ }
+
+ assert {
+ condition = resource.coder_app.supabase[0].url == "https://supabase.com/dashboard/project/abcdefghijklmnop"
+ error_message = "coder_app URL should include project reference"
+ }
+}
+
+run "test_supabase_with_project_dir" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-project-dir"
+ project_ref = "abcdefghijklmnop"
+ project_dir = "/home/coder/my-app"
+ }
+
+ assert {
+ condition = var.project_dir == "/home/coder/my-app"
+ error_message = "project_dir should be set correctly"
+ }
+
+ assert {
+ condition = var.project_ref == "abcdefghijklmnop"
+ error_message = "project_ref should be set correctly"
+ }
+}
+
+run "test_supabase_app_disabled" {
+ command = apply
+
+ variables {
+ agent_id = "test-agent-no-app"
+ dashboard_app = false
+ }
+
+ assert {
+ condition = var.dashboard_app == false
+ error_message = "dashboard_app should be false"
+ }
+
+ assert {
+ condition = length(resource.coder_app.supabase) == 0
+ error_message = "coder_app should not be created when dashboard_app is false"
+ }
+}
+
+run "test_supabase_skip_install" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-skip"
+ skip_install = true
+ }
+
+ assert {
+ condition = var.skip_install == true
+ error_message = "skip_install should be true"
+ }
+}
+
+run "test_supabase_custom_download_url" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-mirror"
+ download_base_url = "https://internal-mirror.corp/supabase/releases"
+ }
+
+ assert {
+ condition = var.download_base_url == "https://internal-mirror.corp/supabase/releases"
+ error_message = "download_base_url should be set to custom mirror"
+ }
+}
+
+run "test_supabase_skip_install_with_token" {
+ command = apply
+
+ variables {
+ agent_id = "test-agent-skip-token"
+ skip_install = true
+ use_external_auth = false
+ access_token = "sbp_test_token_for_skip"
+ }
+
+ assert {
+ condition = var.skip_install == true
+ error_message = "skip_install should be true"
+ }
+}
+
+
diff --git a/registry/coder/modules/supabase/scripts/install.sh.tftpl b/registry/coder/modules/supabase/scripts/install.sh.tftpl
new file mode 100644
index 000000000..f36efa965
--- /dev/null
+++ b/registry/coder/modules/supabase/scripts/install.sh.tftpl
@@ -0,0 +1,273 @@
+#!/bin/bash
+set -euo pipefail
+
+MODULE_DIR="$${HOME}/.coder-modules/coder/supabase"
+LOG_DIR="$${MODULE_DIR}/logs"
+BIN_DIR="$${MODULE_DIR}/bin"
+mkdir -p "$${LOG_DIR}" "$${BIN_DIR}"
+
+SKIP_INSTALL='${ARG_SKIP_INSTALL}'
+INSTALL_METHOD='${ARG_INSTALL_METHOD}'
+VERSION='${ARG_VERSION}'
+PROJECT_REF='${ARG_PROJECT_REF}'
+PROJECT_DIR='${ARG_PROJECT_DIR}'
+DOWNLOAD_BASE_URL='${ARG_DOWNLOAD_BASE_URL}'
+ACCESS_TOKEN=$(echo -n '${ARG_ACCESS_TOKEN}' | base64 -d)
+
+detect_platform() {
+ ARCH=$(uname -m)
+ case "$${ARCH}" in
+ x86_64 | amd64) ARCH="amd64" ;;
+ aarch64 | arm64) ARCH="arm64" ;;
+ *)
+ echo "Error: Unsupported architecture: $${ARCH}"
+ exit 1
+ ;;
+ esac
+
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
+ case "$${OS}" in
+ linux | darwin) ;;
+ mingw* | msys* | cygwin*)
+ OS="windows"
+ ;;
+ *)
+ echo "Error: Unsupported OS: $${OS}. Only linux, darwin, and windows are supported."
+ exit 1
+ ;;
+ esac
+
+ echo "Detected platform: $${OS}/$${ARCH}"
+}
+
+fetch_to_file() {
+ local dest="$1" url="$2"
+ if command -v curl > /dev/null 2>&1; then
+ curl -fsSL --retry 5 --retry-delay 5 -o "$${dest}" "$${url}"
+ elif command -v wget > /dev/null 2>&1; then
+ wget -q -O "$${dest}" "$${url}"
+ elif command -v busybox > /dev/null 2>&1; then
+ busybox wget -q -O "$${dest}" "$${url}"
+ else
+ echo "Error: curl, wget, or busybox is required"
+ return 1
+ fi
+}
+
+resolve_version() {
+ local version="$${VERSION}"
+ if [ "$${version}" = "latest" ]; then
+ local api_response
+ if command -v curl > /dev/null 2>&1; then
+ api_response=$(curl -fsSL "https://api.github.com/repos/supabase/cli/releases/latest")
+ elif command -v wget > /dev/null 2>&1; then
+ api_response=$(wget -qO- "https://api.github.com/repos/supabase/cli/releases/latest")
+ else
+ echo "Error: curl or wget required for version resolution"
+ exit 1
+ fi
+ version=$(echo "$${api_response}" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*"v?([0-9][^"]*).*/\1/')
+ echo "Resolved latest version: $${version}" >&2
+ fi
+ echo "$${version}"
+}
+
+setup_path() {
+ local bin_path="$1"
+
+ if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ]; then
+ ln -sf "$${bin_path}" "$${CODER_SCRIPT_BIN_DIR}/supabase"
+ echo "Linked supabase to CODER_SCRIPT_BIN_DIR"
+ fi
+
+ local bin_dir
+ bin_dir=$(dirname "$${bin_path}")
+ for profile in "$${HOME}/.profile" "$${HOME}/.bash_profile" "$${HOME}/.bashrc" "$${HOME}/.zprofile" "$${HOME}/.zshrc"; do
+ if [ -f "$${profile}" ]; then
+ if ! grep -q "$${bin_dir}" "$${profile}" 2>/dev/null; then
+ echo "export PATH=\"\$PATH:$${bin_dir}\"" >> "$${profile}"
+ echo "Added $${bin_dir} to $${profile}"
+ fi
+ fi
+ done
+
+ local fish_config="$${HOME}/.config/fish/config.fish"
+ if [ -f "$${fish_config}" ]; then
+ if ! grep -q "$${bin_dir}" "$${fish_config}" 2>/dev/null; then
+ echo "fish_add_path $${bin_dir}" >> "$${fish_config}"
+ echo "Added $${bin_dir} to $${fish_config}"
+ fi
+ fi
+}
+
+install_binary() {
+ detect_platform
+ local version
+ version=$(resolve_version)
+
+ local download_url="$${DOWNLOAD_BASE_URL}/v$${version}/supabase_$${OS}_$${ARCH}.tar.gz"
+
+ local tmp_dir
+ tmp_dir=$(mktemp -d)
+ trap "rm -rf $${tmp_dir}" EXIT
+
+ echo "Downloading Supabase CLI v$${version} from $${download_url}..."
+ fetch_to_file "$${tmp_dir}/supabase.tar.gz" "$${download_url}"
+
+ tar -xzf "$${tmp_dir}/supabase.tar.gz" -C "$${tmp_dir}"
+ mv "$${tmp_dir}/supabase" "$${BIN_DIR}/supabase"
+ chmod +x "$${BIN_DIR}/supabase"
+
+ setup_path "$${BIN_DIR}/supabase"
+}
+
+install_native_package() {
+ detect_platform
+ local version
+ version=$(resolve_version)
+
+ local tmp_dir
+ tmp_dir=$(mktemp -d)
+ trap "rm -rf $${tmp_dir}" EXIT
+
+ if command -v dpkg > /dev/null 2>&1; then
+ local pkg_url="$${DOWNLOAD_BASE_URL}/v$${version}/supabase_$${version}_$${OS}_$${ARCH}.deb"
+ echo "Installing via dpkg..."
+ fetch_to_file "$${tmp_dir}/supabase.deb" "$${pkg_url}"
+ if command -v sudo > /dev/null 2>&1; then
+ sudo dpkg -i "$${tmp_dir}/supabase.deb" || {
+ echo "Warning: dpkg install failed, trying binary fallback"
+ install_binary
+ return
+ }
+ else
+ dpkg -i "$${tmp_dir}/supabase.deb" 2>/dev/null || {
+ echo "Warning: dpkg install failed (no sudo), trying binary fallback"
+ install_binary
+ return
+ }
+ fi
+ elif command -v rpm > /dev/null 2>&1; then
+ local pkg_url="$${DOWNLOAD_BASE_URL}/v$${version}/supabase_$${version}_$${OS}_$${ARCH}.rpm"
+ echo "Installing via rpm..."
+ fetch_to_file "$${tmp_dir}/supabase.rpm" "$${pkg_url}"
+ if command -v sudo > /dev/null 2>&1; then
+ sudo rpm -i "$${tmp_dir}/supabase.rpm" || {
+ echo "Warning: rpm install failed, trying binary fallback"
+ install_binary
+ return
+ }
+ else
+ rpm -i "$${tmp_dir}/supabase.rpm" 2>/dev/null || {
+ echo "Warning: rpm install failed (no sudo), trying binary fallback"
+ install_binary
+ return
+ }
+ fi
+ elif command -v apk > /dev/null 2>&1; then
+ local pkg_url="$${DOWNLOAD_BASE_URL}/v$${version}/supabase_$${version}_$${OS}_$${ARCH}.apk"
+ echo "Installing via apk..."
+ fetch_to_file "$${tmp_dir}/supabase.apk" "$${pkg_url}"
+ if command -v sudo > /dev/null 2>&1; then
+ sudo apk add --allow-untrusted "$${tmp_dir}/supabase.apk" || {
+ echo "Warning: apk install failed, trying binary fallback"
+ install_binary
+ return
+ }
+ else
+ apk add --allow-untrusted "$${tmp_dir}/supabase.apk" 2>/dev/null || {
+ echo "Warning: apk install failed (no sudo), trying binary fallback"
+ install_binary
+ return
+ }
+ fi
+ else
+ echo "No supported package manager found, falling back to binary install"
+ install_binary
+ fi
+}
+
+install_brew() {
+ if ! command -v brew > /dev/null 2>&1; then
+ echo "Error: Homebrew is required for install_method=brew"
+ exit 1
+ fi
+ echo "Installing via Homebrew..."
+ brew install supabase/tap/supabase || brew upgrade supabase/tap/supabase || true
+}
+
+install_scoop() {
+ if ! command -v scoop > /dev/null 2>&1; then
+ echo "Error: Scoop is required for install_method=scoop"
+ exit 1
+ fi
+ echo "Installing via Scoop..."
+ scoop bucket add supabase https://github.com/supabase/scoop-bucket.git 2>/dev/null || true
+ scoop install supabase || scoop update supabase || true
+}
+
+install_supabase_cli() {
+ echo "Installing Supabase CLI (method: $${INSTALL_METHOD}, version: $${VERSION})..."
+
+ if [ "$${INSTALL_METHOD}" = "detect" ]; then
+ if command -v brew > /dev/null 2>&1; then
+ INSTALL_METHOD="brew"
+ elif command -v scoop > /dev/null 2>&1; then
+ INSTALL_METHOD="scoop"
+ elif command -v dpkg > /dev/null 2>&1 || command -v rpm > /dev/null 2>&1 || command -v apk > /dev/null 2>&1; then
+ INSTALL_METHOD="native"
+ else
+ INSTALL_METHOD="binary"
+ fi
+ echo "Detected install method: $${INSTALL_METHOD}"
+ fi
+
+ case "$${INSTALL_METHOD}" in
+ brew) install_brew ;;
+ scoop) install_scoop ;;
+ native) install_native_package ;;
+ binary) install_binary ;;
+ *)
+ echo "Error: Unknown install method: $${INSTALL_METHOD}" >&2
+ exit 1
+ ;;
+ esac
+}
+
+if [ "$${SKIP_INSTALL}" != "true" ]; then
+ install_supabase_cli
+else
+ echo "Skipping Supabase CLI installation (skip_install=true)"
+fi
+
+SUPABASE_BIN=""
+if command -v supabase > /dev/null 2>&1; then
+ SUPABASE_BIN="supabase"
+elif [ -x "$${BIN_DIR}/supabase" ]; then
+ SUPABASE_BIN="$${BIN_DIR}/supabase"
+fi
+
+if [ -z "$${SUPABASE_BIN}" ]; then
+ echo "Error: Supabase CLI not found. Ensure it is installed or skip_install is false." >&2
+ exit 1
+fi
+
+echo "✓ Supabase CLI available: $($${SUPABASE_BIN} --version)"
+
+if [ -n "$${ACCESS_TOKEN}" ]; then
+ echo "Logging in with access token..."
+ $${SUPABASE_BIN} login --token "$${ACCESS_TOKEN}"
+ echo "✓ Logged in to Supabase"
+fi
+
+if [ -n "$${PROJECT_REF}" ]; then
+ LINK_DIR="$${PROJECT_DIR:-$${HOME}}"
+ mkdir -p "$${LINK_DIR}"
+ echo "Linking to Supabase project: $${PROJECT_REF} in $${LINK_DIR}..."
+ cd "$${LINK_DIR}"
+ if $${SUPABASE_BIN} link --project-ref "$${PROJECT_REF}" 2>&1; then
+ echo "✓ Linked to project $${PROJECT_REF}"
+ else
+ echo "Warning: Failed to link to project $${PROJECT_REF}. You may need to run 'supabase link --project-ref $${PROJECT_REF}' manually."
+ fi
+fi