Snapgram: rubric and defend your codeswift-6.4/ios-26
Lesson 7 / 7
Unit 25 · Snapgram

Snapgram: rubric and defend your code

Note

Unit 25 · Project 5, rubric. Score yourself honestly, then answer the defend-your-code prompts as if you were in a design review for the data layer. The backend rows here — RLS and concurrency safety — are the ones a senior reviewer will press hardest on.

Snapgram is the project where "it works on my simulator" is most dangerous, because the two things that can be silently wrong — an open RLS policy and a data race — both pass a casual test. This rubric and viva are about proving they're right, not just green. Run /ship-check first, then work the table.

Rubric

Score each row: 2 solid, 1 partial, 0 missing. Aim for 18+ of 24.

# Criterion What "solid" looks like
1 Auth service injected AuthViewModel takes an AuthService protocol; session restored at launch
2 No secrets leaked Only the anon key in the app; no service-role key; keys not committed
3 RLS enabled everywhere Every table has RLS on with explicit policies; verified in the SQL editor
4 Correct policy clauses using for read/delete, with check for insert; "write only your own" enforced
5 Codable rows Postgres snake_case mapped cleanly; models are Sendable structs
6 Storage secured Path-per-user; insert policy restricts writes to the user's own folder
7 Store path, derive URL Rows hold image_path; URLs computed at render, fed to the Project 4 loader
8 Cursor pagination Keyset by created_at; no repeats/skips when new posts arrive
9 Pull-to-refresh .refreshable awaits the fetch; spinner tracks real work
10 Optimistic like Local update first, network call, rollback on failure
11 Concurrency-safe caches Shared mutable state is an actor; no @unchecked Sendable without a written why
12 Reviewed AI diffs You ran /swift-review, and caught the checkpoint's seeded race

Defend your code

Why must the feed cache be an actor? Because it's shared mutable state written from many concurrent tasks (the prefetch task group). Without serialization, concurrent dictionary inserts interleave — lost writes, torn storage, or a crash — a data race that's undefined behavior. An actor gives the cache its own isolation domain so only one task touches store at a time, and it's genuinely Sendable so it can be shared safely. A plain class would need a hand-written lock; the actor is the lock, checked by the compiler.

What RLS policy protects user data, and how do you know it works? Read policies (for select using (true)) let any authenticated user see the feed; write policies (for insert with check (auth.uid() = author_id) and the matching delete/like policies) let a user only write rows they own. You know it works by testing the negative: attempt to insert a post with another user's author_id, or delete someone else's post, from the SQL editor as the anon role — the database must reject both. Passing writes for yourself isn't proof; rejected writes for others is.

Why is the anon key in your shipped app not a security hole? Because the anon key is constrained by RLS — it can only perform operations your policies permit. Security lives in the database, not in the client holding the key. This is exactly why RLS being on with correct policies is non-negotiable: the anon key's safety depends on it.

Where did the seeded checkpoint bug hide, and why did tests miss it? In an @unchecked Sendable cache mutated concurrently with no synchronization. Tests missed it because a data race is nondeterministic — with few items on a fast machine, the corrupting interleaving usually doesn't occur. It scales with load and luck, which small unit tests don't reproduce. The @unchecked annotation was the tell.

Why is the optimistic like safe to roll back? Because Post is a value type: wasLiked captured before the mutation is a complete snapshot of the prior state, so reversing the change on failure is exact. Value semantics make optimistic UI trivial — no defensive copy, no shared reference someone else might have changed underneath you.

Carry-forward

You now have the full backend spine: auth with sessions, a Postgres schema secured by RLS, storage with per-user policies, keyset pagination, and concurrency-safe caching. The Professional-stage projects (Staybnb, Loops) build on exactly this — more tables, more policies, more concurrency — but the reasoning is identical: security as data policy, shared mutable state behind actors, UI state on the main actor, dependencies behind protocols. What changes at Professional is the AI autonomy, not the correctness bar. Keep reviewing every diff.

Knowledge check

Q: A reviewer sees @unchecked Sendable on a class in your PR. What should they ask? "Where is the synchronization, and where is the written justification?" @unchecked Sendable turns off the compiler's safety check, so it's only correct if the type is made safe by other means (a lock, proven immutability) and that reasoning is documented. Mutable state with neither is a race.

Q: How do you prove an RLS policy is correct? Test the negative case. Confirm that a user cannot read or write data they shouldn't — insert as another user, delete another user's row — and see the database reject it. Successful self-writes don't prove the policy; rejected cross-user writes do.