Date: 2026-05-16
Branch: develop
Commit: c046d4ae
Every call to POST /v1/posts/{post_id}/reactions fired 2–3 GetStream API calls:
reactions.filter(activity_id, user_id)— scan for existing reactions by this userreactions.delete(reaction["id"])— for each existing reaction found (replace case)reactions.add(...)— the actual write
Every call to DELETE /v1/reactions/{reaction_id} fired:
reactions.get(reaction_id)— read to verify ownership and fetchpost_idreactions.delete(reaction_id)— the actual delete
There was no local record of reactions, so every mutation required asking GetStream first.
One row per (post_id, profile_id) — enforced by a unique constraint.
Caches the stream_reaction_id so we can delete by ID without a prior read.
CREATE TABLE post_reactions (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
profile_id INTEGER NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
kind VARCHAR(50) NOT NULL,
stream_reaction_id VARCHAR(200) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT uq_post_reaction_post_profile UNIQUE (post_id, profile_id)
);| Step | Before | After |
|---|---|---|
| Check for existing reaction | reactions.filter(activity_id, user_id) → GetStream read |
SELECT … WHERE post_id=? AND profile_id=? → Postgres read |
| Delete old reaction (replace) | reactions.delete(id) per result |
reactions.delete(stream_reaction_id) — single targeted delete, ID already known |
| Write new reaction | reactions.add(...) |
unchanged |
| Persist cache | — | INSERT INTO post_reactions |
GetStream calls: 3 → 1 (replace case) or 2 → 1 (new reaction).
| Step | Before | After |
|---|---|---|
| Verify ownership + get post_id | reactions.get(reaction_id) → GetStream read |
SELECT … WHERE stream_reaction_id=? AND profile_id=? → Postgres read |
| Delete | reactions.delete(reaction_id) |
unchanged |
| Clean up cache | — | DELETE FROM post_reactions WHERE id=? |
GetStream calls: 2 → 1.
| File | Change |
|---|---|
app/db/models.py |
Added PostReaction SQLModel |
app/sherpahealthy/reactions.py |
Rewrote create_reaction and delete_reaction to use Postgres cache |
supabase/migrations/20260516010000_add_post_reactions_table.sql |
Dev/staging migration |
alembic/versions/b8c9d0e1f2a3_add_post_reactions_table.py |
Production migration (merges both current heads) |
Added a SITEMAP_ENABLED (default false) feature flag.
/sitemap*.xml and /robots.txt return 401 unless the flag is true.
Set to true only in the production configmap — prevents dev/staging content from being indexed.
Added to tests/conftest.py. Patches get_stream_client in every module that imports it and stubs find_post_activity_id. All test runs now execute zero real GetStream API calls.
Modules patched:
app.sherpahealthy.feeds(source)app.sherpahealthy.reactionsapp.sherpahealthy.commentsapp.sherpahealthy.posts.commentsapp.sherpahealthy.posts.mainapp.sherpahealthy.full_postsapp.sherpahealthy.posts.feeds_http
get_post_reactions(the public read endpoint) still queries GetStream directly — it returns full reaction lists with profile data and is not on the hot write path.post_reactionshasON DELETE CASCADEon both FKs, so cleanup on user/post deletion is automatic; no explicit cleanup needed in application code.- The unique constraint prevents duplicate rows even under concurrent requests.
stream_reaction_idis sourced from GetStream'sreactions.add()response and stored as-is.