Clix + Supabase Integration

This guide shows how to integrate Clix push notifications into a Supabase project using Edge Functions and Postgres triggers. You will:
  • Add a reusable ClixClient inside supabase/functions/_shared
  • (Recommended) Create helper builders for ClixNotification payloads
  • Deploy Edge Functions that send notifications
  • Optionally wire automatic dispatch via database triggers with pg_net

1. Prerequisites

  • A Supabase project (with CLI installed) – https://supabase.com/docs/guides/cli
  • A Clix project (obtain Project ID + Secret API Key)
  • Supabase CLI authenticated: supabase login
  • (If using triggers) Postgres extension pg_net enabled (we’ll cover this below)

2. Configure Secrets / Environment Variables

Store credentials as Supabase Function secrets (never hard‑code keys):
Set Supabase Edge Function secrets
supabase secrets set \
  CLIX_PROJECT_ID=your_clix_project_id \
  CLIX_API_KEY=your_clix_secret_api_key \
  CLIX_API_BASE=https://api.clix.so

# Existing required Supabase vars (examples) – ensure already set
supabase secrets set \
  SUPABASE_URL=https://your-project-ref.supabase.co \
  SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
Docs:

3. Add the Shared Clix Client

Create the file below at supabase/functions/_shared/clix_client.ts.
supabase/functions/_shared/clix_client.ts
// Lightweight reusable Clix notification client for Deno Edge Functions
// Uses project_user_id targeting instead of device_id

export interface ClixNotification {
  target: { project_user_id: string };
  title: string;
  body: string;
  image_url?: string;
  landing_url?: string;
}

export interface ClixResult {
  delivery_results?: unknown;
  [k: string]: unknown;
}

export class ClixClient {
  private projectId: string;
  private apiKey: string;
  private base: string;
  constructor(opts?: {
    projectId?: string;
    apiKey?: string;
    baseUrl?: string;
  }) {
    this.projectId = opts?.projectId || Deno.env.get("CLIX_PROJECT_ID") || "";
    this.apiKey = opts?.apiKey || Deno.env.get("CLIX_API_KEY") || "";
    this.base = (
      opts?.baseUrl ||
      Deno.env.get("CLIX_API_BASE") ||
      "https://api.clix.so"
    ).replace(/\/$/, "");
    if (!this.projectId || !this.apiKey) {
      throw new Error("ClixClient missing credentials");
    }
  }

  async send(
    notifications: ClixNotification[]
  ): Promise<{ ok: boolean; result: ClixResult; status: number }> {
    const res = await fetch(`${this.base}/api/v1/messages:send`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Clix-Project-ID": this.projectId,
        "X-Clix-API-Key": this.apiKey,
      },
      body: JSON.stringify({ push_notifications: notifications }),
    });
    const json: ClixResult = await res.json().catch(() => ({}));
    return { ok: res.ok, status: res.status, result: json };
  }
}
Instead of hand‑crafting notification objects each time, centralize builders for consistent structure and future enrichment.
supabase/functions/_shared/notification_scenarios.ts
import { ClixNotification } from "./clix_client.ts";

// Multiple recipients for a completed todo
export function buildTodoCompletedNotifications(
  projectUserIds: string[],
  todo: { id: number; title: string }
): ClixNotification[] {
  return projectUserIds.map((pid) => ({
    target: { project_user_id: pid },
    title: "Todo Completed",
    body: todo.title || `Todo #${todo.id} was completed`,
    landing_url: `https://example.com/todos/${todo.id}`,
  }));
}

5. Edge Function: Notify Todo Completion

Minimal example: accept a todo_id and a target project_user_id, build a notification, and send it via ClixClient (no database lookup logic included here).
supabase/functions/notify-todo/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import {
  ClixClient,
  buildTodoCompletedNotification,
} from "../_shared/clix_client.ts";

// Minimal: requires env secrets (CLIX_PROJECT_ID, CLIX_API_KEY)
serve(async (req: Request) => {
  if (req.method !== "POST") return json({ error: "Only POST" }, 405);
  const { todo_id, project_user_id, title } = await req
    .json()
    .catch(() => ({}));
  if (!todo_id) return json({ error: "todo_id required" }, 400);
  if (!project_user_id) return json({ error: "project_user_id required" }, 400);

  // Build a minimal todo shape for the helper (no DB fetch)
  const todo = { id: todo_id, title: title || "" };
  const clix = new ClixClient();
  const notification = buildTodoCompletedNotification(project_user_id, todo);
  const { ok, result, status } = await clix.send([notification]);
  if (!ok) return json({ error: "clix send failed", details: result }, status);
  return json({ ok: true, sent: 1, clix: result });
});

function json(data: unknown, status = 200) {
  return new Response(JSON.stringify(data), {
    status,
    headers: { "Content-Type": "application/json" },
  });
}

Deploy the Function

supabase functions deploy notify-todo --no-verify-jwt
(You can require JWT if only your backend calls this. See auth function docs: https://supabase.com/docs/guides/functions/auth)

6. Calling the Edge Function (Invocation Options)

You can integrate notify-todo in three primary ways—choose based on coupling, complexity, and operational preferences. (Code snippets intentionally omitted.)
  1. Database Trigger (pg_net): Attach a lightweight trigger to the todos table so when a row transitions to completed, Postgres performs an HTTP POST to the deployed Edge Function. Pros: automatic, low latency, no extra app call. Cons: trigger logic inside SQL, needs pg_net and careful key handling.
  2. Direct API Call: From your backend (or another Edge Function) perform an authenticated POST with the todo_id. Pros: simplest to reason about in application code, easier to add conditional logic or retries. Cons: requires your app layer to explicitly invoke for every event.
  3. Scheduled / Batch Orchestrator: Periodic job (cron, scheduled function, worker) queries for recently completed todos and sends notifications in batches (reduces per-event overhead). Pros: efficient for high volume; can deduplicate. Cons: not real-time; added scheduling complexity.
Selection guidance:
  • Want instant, automatic dispatch on state change? Use a trigger.
  • Need business rules (feature flags, throttling, analytics gating)? Use direct API call.
  • Need aggregation (daily digests, rate limiting)? Use scheduled batch.

7. Testing the Flow

  1. Insert / update a todo to status completed
  2. Confirm notification_dispatch_log entries
  3. Verify successful HTTP responses & Clix dashboard reception
  4. For manual test call the function:
curl -X POST \
  https://<project-ref>.functions.supabase.co/notify-todo \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" \
  -d '{"todo_id": 123}'

8. Security & Best Practices

  • Put your Clix Secret API Key only in Supabase secrets (never ship to clients)
  • Prefer trigger-based automation for reliability; fall back to client calls when immediacy or dynamic logic is required
  • Use structured builders to evolve payloads without touching every function
  • Log responses (already in examples) for observability; consider adding retention policies

9. Useful Supabase Docs

10. Summary

You now have:
  • A reusable ClixClient consuming environment secrets
  • Scenario helpers for consistent notifications
  • Edge Functions (notify-todo) to send messages
  • Optional Postgres triggers to automatically invoke those functions
Iterate by adding more scenario builders (e.g. silent data sync, milestone achievements) and enrich payloads with custom analytics fields.