Comments
Comments on any record, from any table, gated by the table's own SELECT access
Overview
Supasheet ships a generic comments system — any record in any table can have a comment thread without you adding new columns or migrations. Comments are stored centrally in supasheet.comments, and access to a table's comment thread is simply whatever SELECT privilege already exists on that table — there's nothing extra to configure per table.
The schema is defined in supabase/migrations/20260514000001_comments.sql.
The supasheet.comments Table
| Column | Type | Purpose |
|---|---|---|
id | uuid | Comment id |
created_at / updated_at | timestamptz | Audit timestamps |
schema_name | text | Schema of the record being commented on |
table_name | text | Table of the record being commented on |
record_id | text | Primary key of the record (stored as text for flexibility) |
content | text | The comment body |
created_by | uuid | Author — references supasheet.users(id) |
Indexes on (schema_name, table_name, record_id), created_by, and created_at keep listings fast.
Comment Access
There's no separate :comment permission to grant, and no per-table setup step. Comment access on a table is simply that table's ordinary SELECT privilege — if a role can SELECT desk.tasks, it can already read and post comments on desk.tasks rows:
- Who can read comments on the table — the table's RLS
SELECTpolicy requires the caller's role to holdSELECTon the underlying table;supasheet.get_comments()enforces the identical rule. - Who can post comments — the table's RLS
INSERTpolicy requires that sameSELECTprivilege as well as the author matchingauth.uid().
See No Separate :audit or :comment Permission for the underlying mechanics.
UPDATE and DELETE are always limited to the original author.
Showing Comments in the UI
There's no metadata to configure — a Comments link appears automatically in a record's actions menu (/$schema/resource/$resource/$resourceId/comment) for any user whose role can SELECT the underlying table.
Anyone who can read desk.tasks can browse its comment thread, post new comments, and edit / delete their own.
Don't confuse this with the detail.tabs metadata field described in Metadata — that field only filters which related-table tabs appear on the detail page (foreign key relationships). Comments and the audit log are separate, access-gated pages, not metadata-configured tabs.
Reading from SQL
Use supasheet.get_comments(schema, table, record_id) to fetch a thread with author details:
SELECT *
FROM supasheet.get_comments('desk', 'tasks', 'a1b2c3…');The function returns each comment joined to author info (name, email, picture_url). Like all Supasheet meta helpers, it runs with security definer but checks the caller's SELECT privilege on the target table internally — there is no way to read a thread on a table you can't already query.
Direct table queries work too — the base table's RLS enforces the exact same rule, so there's no difference in what you can see, only whether you get author details joined in for free:
-- Every comment on a specific record — every row where you can SELECT
-- the target table, not just the ones you authored
SELECT * FROM supasheet.comments
WHERE schema_name = 'desk'
AND table_name = 'tasks'
AND record_id = 'a1b2c3…'
ORDER BY created_at;The base table's own RLS policy is has_table_privilege(current_user, format('%I.%I', schema_name, table_name), 'select') — the identical rule the get_comments() helper checks internally. Reach for the helper when you want author details (created_by_name/email/picture_url) joined in; query the table directly otherwise. Neither one shows you a narrower or wider set of comments than the other.
Posting Comments
The INSERT policy enforces two checks:
created_by = auth.uid()— you can only post as yourself- The current role has
SELECTon the target table (checked viahas_table_privilege)
INSERT INTO supasheet.comments
(schema_name, table_name, record_id, content, created_by)
VALUES
('desk', 'tasks', 'a1b2c3…', 'I picked this up — ETA Friday.', auth.uid());In the UI, this happens automatically when a user submits the comment form on the resource detail page.
Editing and Deleting
Authors can edit and delete their own comments. Admins can be granted the same by extending the default RLS policies with a role-based override (e.g. pg_has_role(current_user, 'x-admin', 'member')) — but typically comments are immutable history, so most teams leave the defaults as-is.
Combining with Notifications
A natural extension is to notify subscribers when someone comments. Add an AFTER INSERT trigger on supasheet.comments that resolves "interested users" (typically the record's assignee / creator) and calls supasheet.create_notification(...):
CREATE OR REPLACE FUNCTION desk.notify_task_comment ()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
DECLARE
v_task_owner uuid;
BEGIN
IF NEW.schema_name = 'desk' AND NEW.table_name = 'tasks' THEN
SELECT assignee_id INTO v_task_owner
FROM desk.tasks WHERE id::text = NEW.record_id;
IF v_task_owner IS NOT NULL AND v_task_owner <> NEW.created_by THEN
PERFORM supasheet.create_notification(
p_type => 'task_comment',
p_title => 'New comment on your task',
p_body => left(NEW.content, 280),
p_user_ids => ARRAY[v_task_owner],
p_link => format('/desk/resource/tasks/%s/comment', NEW.record_id)
);
END IF;
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER trg_notify_task_comment
AFTER INSERT ON supasheet.comments
FOR EACH ROW EXECUTE FUNCTION desk.notify_task_comment();See Notifications for the full API.
Access Cheatsheet
| Goal | Required grant |
|---|---|
| See the Comments tab on a record | SELECT on <schema>.<table> |
| Post a comment on a record | SELECT on <schema>.<table> |
| Edit / delete your own comment | None beyond above |
| Edit / delete someone else's comment | Custom — extend the default policies |
Next Steps
- Notifications — Notify users when new comments land
- Audit Logs — Pair comments with a full change history
- Authorization — How roles, grants, and RLS work together