Optional PostgREST audit logging (Supabase)
Version: 1.0
Date: April 2, 2026
Owner: Platform / AI Team
1. Purpose
Pattern for append-only audit rows written from Node services via Supabase PostgREST (POST /rest/v1/<table>). Logging must be optional (missing env ⇒ no-op), non-blocking for the user-facing path, and safe under RLS when using the service role key only on the server.
In-repo example: Brainforge Slack Assistant — brainforge_assistant_interaction_log (see apps/slack-apps/brainforge-assistant/src/interaction-log.js and apps/slack-apps/brainforge-assistant/ENVIRONMENT.md).
2. Environment variables
- Use service-specific names when a deploy might target a different Supabase project than the Forge (e.g.
ASSISTANT_INTERACTION_LOG_SUPABASE_URL+ASSISTANT_INTERACTION_LOG_SUPABASE_SERVICE_ROLE_KEY). - When the target is always Internal AI Core, document that operators may mirror platform vars: same values as
BF_SUPABASE_URLandBF_SUPABASE_SERVICE_KEY. - Never commit keys. Load from deployment secrets or local
.env(gitignored).
3. Implementation checklist
| Practice | Why |
|---|---|
| Gate on both URL and key | If either is unset, return immediately; do not throw. |
| Fire-and-forget on hot paths | Wrap async insert so failures never reject the Slack/API handler; log warnings only if useful. |
| Truncate large text fields | Cap user_message, model_prompt, answer_text, etc., to avoid huge rows and PostgREST limits. |
| Normalize structured fields | Cap array length (e.g. citations); coerce unknown shapes to safe JSON. |
| HTTPS client with timeout | Use a bounded timeout (e.g. 12s); destroy the request on timeout. |
Handle IncomingMessage errors | Attach res.on('error', …) so an aborted response does not crash the process. |
Prefer Prefer: return=minimal | Reduces payload on success. |
| Headers | apikey, Authorization: Bearer <service_role>, Content-Type: application/json. |
4. Database and RLS
- Migration in
apps/platform/supabase/migrations/(or the target project’s migration flow) with explicit columns, checks, and indexes for query patterns (created_at, foreign keys to Slack ids if needed). - RLS enabled with no broad
anon/authenticatedpolicies if rows contain sensitive text; service role bypasses RLS for server inserts (and optional internal read tools). - Treat tables as append-only by convention; document whether deletes are allowed for GDPR/support.
5. Privacy, retention, and access
- Document what is stored (e.g. user prompts, model output, Slack ids).
- Align retention and who may query with internal policy (People Ops / Security / Platform).
- For assistants: see
apps/slack-apps/brainforge-assistant/ENVIRONMENT.md(optional audit section).
6. Verifying locally
- Do not rely on
source .envfor a large monorepo.env; malformed or complex lines can leave variables empty in bash while Node still parses them. Prefer:node --env-file=/path/to/.env -e "…"orrequire('dotenv').config({ path: '…' })in a small script.
- Smoke test:
POSTone row withcurlor a script, thenSELECTvia Supabase MCPexecute_sqlor SQL editor; delete test rows if policy allows.
7. Assistant-specific troubleshooting (brainforge_assistant_interaction_log)
When debugging the Slack assistant (apps/slack-apps/brainforge-assistant), use this quick triage flow:
- Confirm both logging env vars are set on the assistant service:
ASSISTANT_INTERACTION_LOG_SUPABASE_URLASSISTANT_INTERACTION_LOG_SUPABASE_SERVICE_ROLE_KEY
- Confirm migration
apps/platform/supabase/migrations/010_brainforge_assistant_interaction_log.sqlwas applied to that same Supabase project. - Check service logs for:
Assistant interaction log: invalid ASSISTANT_INTERACTION_LOG_SUPABASE_URLAssistant interaction log failed: PostgREST <status>Assistant interaction log timeout
- Ensure URL/key point to the same project (cross-project URL/key pairs are a common cause of silent insert failures).
Row semantics used by runtime:
- One row per Q&A turn (
sourceisapp_mention,direct_message, orslash_command). status='ok'means model generation + Slack delivery succeeded.status='error'means the turn failed. If the model produced output but later steps failed,metadata.failureStage = "post_model"and partial model fields are preserved.
Useful queries:
-- Recent logging failures
SELECT created_at, source, slack_channel_id, slack_user_id, error_message, metadata
FROM brainforge_assistant_interaction_log
WHERE status = 'error'
ORDER BY created_at DESC
LIMIT 50;-- Failures after model output was produced
SELECT created_at, source, model, azure_response_id, metadata
FROM brainforge_assistant_interaction_log
WHERE status = 'error'
AND metadata->>'failureStage' = 'post_model'
ORDER BY created_at DESC
LIMIT 50;Related
- Supabase Query Access — projects,
BF_SUPABASE_*, MCPproject_id. - databases.md — Internal AI Core tables overview.
- 1Password CLI — service role from vault.