circles
Overview
Circles are small, named groups that users create to organize friends around shared contexts (e.g., "Book Club", "Work Friends"). Each circle has a creator (created_by), an optional description, and name/description length constraints. Circle membership is invitation-based and admin-controlled. Circles serve as audience scopes for posts.
Relevant Invariants
- Invariant #7: "Human-Scale Social Contexts" -- Circles enforce small-group contexts
- Invariant #1: "Participation Is Always Voluntary" -- Circle membership is invitation-based
Schema
-- From 20260131_circles_schema.sql + 20260208_create_circle_rpc.sql
CREATE TABLE circles (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL CHECK (char_length(name) <= 50),
description TEXT DEFAULT NULL CHECK (char_length(description) <= 200),
created_by UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
circle_type TEXT DEFAULT 'static' CHECK (circle_type IN ('static', 'dynamic')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Columns
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | No | gen_random_uuid() | Primary key |
name | text | No | -- | Circle display name (max 50 characters) |
description | text | Yes | NULL | Optional circle description (max 200 characters) |
created_by | uuid | No | -- | Circle creator, references profiles(id) |
circle_type | text | Yes | 'static' | Circle type: 'static' (manual membership) or 'dynamic' (rule-based) |
created_at | timestamptz | No | NOW() | Creation timestamp |
updated_at | timestamptz | No | NOW() | Last update timestamp (auto-updated by trigger) |
Constraints
CHECK (char_length(name) <= 50)-- Name limited to 50 charactersCHECK (char_length(description) <= 200)-- Description limited to 200 charactersCHECK (circle_type IN ('static', 'dynamic'))-- Valid circle types
RPC Functions
-- Creates a circle and adds creator as admin member atomically
-- SECURITY DEFINER to bypass the chicken-and-egg RLS problem
-- (circle_members INSERT requires admin, but creator isn't a member yet)
create_circle(p_name TEXT, p_description TEXT DEFAULT NULL) RETURNS UUID
Triggers
-- Auto-updates updated_at on row changes
CREATE TRIGGER circles_updated
BEFORE UPDATE ON circles
FOR EACH ROW EXECUTE FUNCTION update_circles_updated_at();
Helper Functions
The circles schema defines three SECURITY DEFINER helper functions used by RLS policies:
-- Check if user is a circle member (status = 'member')
is_circle_member(circle_uuid UUID, user_uuid UUID) RETURNS BOOLEAN
-- Check if user is a circle admin (role = 'admin' AND status = 'member')
is_circle_admin(circle_uuid UUID, user_uuid UUID) RETURNS BOOLEAN
-- Check if user has any access (member or invited)
has_circle_access(circle_uuid UUID, user_uuid UUID) RETURNS BOOLEAN
RLS Policies
-- SELECT: Members and invitees can view their circles
CREATE POLICY "Circle members can view circle"
ON circles FOR SELECT TO authenticated
USING (has_circle_access(id, auth.uid()));
-- INSERT: Authenticated users can create circles (must set created_by to self)
CREATE POLICY "Users can create circles"
ON circles FOR INSERT TO authenticated
WITH CHECK (auth.uid() = created_by);
-- UPDATE: Only circle admins can update
CREATE POLICY "Circle admins can update"
ON circles FOR UPDATE TO authenticated
USING (is_circle_admin(id, auth.uid()));
-- DELETE: Only circle admins can delete
CREATE POLICY "Circle admins can delete"
ON circles FOR DELETE TO authenticated
USING (is_circle_admin(id, auth.uid()));
Related
- circle_members -- Membership records for this circle
- circle_feed_state -- Per-user feed pacing within circle
- post_circles -- Posts targeted to this circle
- profiles -- Circle creator
Last updated: 2026-02-07