---
name: propozel-integration
description: >
  How agents in this company read from and write to Propozel — the system
  of record at propozel.com. Covers auth, content schema, RLS-safe write
  patterns, idempotency, retry policy. Read whenever any agent makes an
  HTTP call to Propozel. Cite by P-rule in any failure or edge-case note.
---

# Propozel Integration — operating manual

Propozel runs on Supabase Postgres + Cloudflare Pages. Agents reach it
via the Supabase REST API (PostgREST), authenticated with a **service
role key** stored in the n-clerk Railway environment. RLS policies still
apply via PostgREST's `--header "Profile: <role>"` mechanism if needed,
but service role bypasses RLS — use carefully.

## P1 — Auth + base URL

```
PROPOZEL_SUPABASE_URL = https://xvnsiggbqfexyvvysnmg.supabase.co
PROPOZEL_SUPABASE_SERVICE_KEY = <stored in Railway, never in git>
PROPOZEL_BASE_URL = https://propozel.com
PROPOZEL_DEFAULT_WORKSPACE_ID = <UUID from Propozel admin>
```

Every API call:

```http
GET/POST/PATCH/DELETE {PROPOZEL_SUPABASE_URL}/rest/v1/<table>
Authorization: Bearer {PROPOZEL_SUPABASE_SERVICE_KEY}
apikey: {PROPOZEL_SUPABASE_SERVICE_KEY}
Content-Type: application/json
Prefer: return=representation
```

**Rule P1.1**: never embed the service key in any committed artifact (git, S3 metadata, Paperclip secrets payload exported to git).
**Rule P1.2**: rotate the service key if any agent log is suspected of leaking it.

## P2 — Content tables (the ones we touch)

| Table | What we do | Key fields we set |
|---|---|---|
| `projects` | READ only (consultant creates projects via Propozel UI) | id, slug, name, lead_consultant, status, share_token |
| `sprints` | READ + occasionally PATCH `executive_summary` and `status` | id, project_id, slug, number, focus, executive_summary, status |
| `content_items` | READ + POST + PATCH (drafts as `unvalidated`, never `validated`) | id, project_id, sprint_id, type, title, summary, detail, validation_status, priority, evidence, tags, owner, cu_tier, cu_value, novelty_rating, complexity_rating, dependency_rating, acceptance_criteria, created_by |
| `sources` | POST mostly (gathered evidence) | project_id, type, title, url, notes, captured_at, captured_by, metadata |
| `comments` | POST (analyst notes to consultant) | item_id, author, content |
| `sprint_items` | POST (per migration 019, sprint-item M2M) | sprint_id, content_item_id |
| `hill_chart_snapshots` | POST occasionally (per migration 020) | project_id, snapshot_at, items[] |
| `share_views` | READ only — never POST | id, project_id, audience, theme, scope |
| `presenter_observations` | POST (when presenter mode active and we capture for the consultant) | source_id linkage |

**Rule P2.1**: never write to tables outside this list without engagement-lead sign-off.
**Rule P2.2**: never DELETE content_items. Demote via `validation_status: needs_clarification` or PATCH `archived_at`. Deletion is a human action.

## P3 — Standard write patterns

### P3.1 — Create a finding

```http
POST {PROPOZEL_SUPABASE_URL}/rest/v1/content_items
Body:
{
  "project_id": "<uuid>",
  "sprint_id": "<active-sprint-uuid>",
  "type": "finding",
  "title": "Mid-market AU buyers prefer monthly contracts over annual",
  "summary": "Three of four interviewed founders treat monthly billing as a deal-maker; Forrester 2025 confirms 64% SMB preference. Annual default lost ~30% of one founder's pipeline.",
  "detail": "<long form with citations>",
  "category": "market",
  "validation_status": "unvalidated",
  "priority": "high",
  "evidence": "src_001 (interview), src_007 (Forrester 2025), src_012 (competitor), src_018 (founder X)",
  "tags": ["pricing", "buying-motion", "sprint-3"],
  "owner": "consultant@example.com",
  "cu_tier": "B",
  "cu_value": 0.62,
  "novelty_rating": 2,
  "complexity_rating": 3,
  "dependency_rating": 2,
  "acceptance_criteria": "If 5+ ANZ buyers in next two interviews choose annual when given a 20% discount, this finding is falsified.",
  "created_by": "agent:finding-analyst"
}

Response (201):
{ "id": "<new uuid>", ... }
```

### P3.2 — Patch a sprint executive summary

```http
PATCH {PROPOZEL_SUPABASE_URL}/rest/v1/sprints?id=eq.<uuid>
Body:
{
  "executive_summary": "<new narrative>"
}
```

### P3.3 — Add a source

```http
POST {PROPOZEL_SUPABASE_URL}/rest/v1/sources
Body:
{
  "project_id": "<uuid>",
  "type": "interview_transcript",
  "title": "Founder of mid-market accounting SaaS — pricing call",
  "url": "<S3 signed URL OR link to Propozel attachment>",
  "notes": "30-min discovery call, focus on billing preference. Key quote: 'monthly is the only way our buyers actually convert.'",
  "captured_at": "2026-04-15T11:30:00Z",
  "captured_by": "agent:discovery-scout",
  "metadata": {
    "confidentiality": "client_confidential",
    "speakers": ["interviewer", "interviewee"],
    "duration_min": 30,
    "captured_during": "discovery_sprint_3"
  }
}
```

### P3.4 — Add a comment

```http
POST {PROPOZEL_SUPABASE_URL}/rest/v1/comments
Body:
{
  "item_id": "<content-item-uuid>",
  "author": "agent:finding-analyst",
  "content": "Stress-tested against F-007 — held up under 2 of 3 counter-scenarios. Demoted CU from 0.78 to 0.62. See engagements/<slug>/stress-test-log.md for detail."
}
```

## P4 — Standard read patterns

### P4.1 — Active sprint for a project

```http
GET {PROPOZEL_SUPABASE_URL}/rest/v1/sprints?project_id=eq.<uuid>&status=eq.active&select=*&order=number.desc&limit=1
```

### P4.2 — Open content_items for a sprint

```http
GET {PROPOZEL_SUPABASE_URL}/rest/v1/content_items?sprint_id=eq.<uuid>&validation_status=in.(unvalidated,needs_clarification)&select=*
```

### P4.3 — Sources for a project, by type

```http
GET {PROPOZEL_SUPABASE_URL}/rest/v1/sources?project_id=eq.<uuid>&type=eq.interview_transcript&select=*&order=captured_at.desc
```

## P5 — Idempotency

**Rule P5.1**: every POST includes `Idempotency-Key` header derived from
`sha256(agent_run_id + content_hash)`. Re-runs detect dupes server-side
(Propozel's API has a deduper for create operations on key tables).

**Rule P5.2**: if a POST returns 409 (conflict — idempotency-key seen),
parse the response to find the existing item ID and continue. Do NOT
retry with a different key.

**Rule P5.3**: PATCH operations are naturally idempotent (we send the
new state, not a delta). Always PATCH with the FULL set of fields you
intend to update.

## P6 — Retry policy

Network blips are real. Apply exponential backoff:

| Status | Action |
|---|---|
| 200/201 | success |
| 409 | dedupe — fetch existing, continue |
| 429 | rate limit — wait 1s, 2s, 4s, 8s, 16s (5 retries max) |
| 500/502/503/504 | server — wait 1s, 4s, 16s (3 retries max), then surface to engagement-lead |
| 401 | auth — STOP all writes, surface immediately (key may have rotated) |
| 403 | permission — STOP, RLS or workspace boundary issue |
| 404 | not found — verify ID hasn't been deleted, surface to engagement-lead |
| 422 | validation — fix the body and re-POST (don't dumb retry) |

**Rule P6.1**: never retry past the limit. Surface the failure to
engagement-lead with the request body + response. Failure mode "I just
keep retrying" is worse than failure mode "I stopped and asked".

## P7 — RLS safety + workspace boundary

Service role bypasses RLS. Use the bypass responsibly:

**Rule P7.1**: every write must include `project_id` matching the
engagement's BRIEF. If the project_id doesn't match, it's a coding
error — refuse and surface.

**Rule P7.2**: never write into a workspace_id you weren't assigned.
Cross-workspace writes are forbidden unless engagement-lead has explicit
human approval written into the engagement BRIEF.

**Rule P7.3**: when in doubt about workspace boundaries, fall back to
PostgREST with `Profile: anon` header to test if RLS would allow it
under the consultant's actual user role. If RLS refuses, your write is
out-of-scope.

## P8 — Schema migration awareness

Propozel migrations land roughly weekly. The session-handoff doc
`docs/session-handoff.md` in the conport repo lists pending + applied
migrations. Read it on Monday cron.

**Rule P8.1**: if you encounter a `column does not exist` error,
re-read the latest migration before guessing schema.
**Rule P8.2**: never write to `content_items.sprint_id` AND `sprint_items`
M2M table without checking which is canonical for this sprint (per
migration 019, M2M is canonical for new data; legacy sprint_id stays
populated for back-compat).

## P9 — Anti-patterns (auto-reject)

- Hard-coding the service key anywhere
- POST without `created_by: 'agent:<slug>'`
- POST with `validation_status: 'validated'`
- PATCH with partial state ("just update title") — always send full intent
- DELETE on any content_items row
- Bypassing this skill ("I'll just hit the Cloudflare Worker function instead")
- Assuming a finding's project_id matches the active engagement without checking
- Retrying past the policy limit
- Writing across workspace_id without engagement-lead written approval
