TidyShelves – Architecture, Sync & Subscriptions
Architecture, Sync, Subscriptions & Onboarding
This page is the single source of truth for TidyShelves server and iOS implementation phases: sync, subscriptions, device limits, onboarding invites, and offline-first UX.
🧩 It reflects what’s already implemented (Phase 0 & Phase 1) and what’s planned next in a server-first sequence.
Current Status: ✅ Phase 0 (iOS local-only) · ✅ Phase 1 (Entity Sync Backend) · ⏳ Subscription, Tenant/User, Invites, Limits & iOS sync pending
🧠 Core Concepts
TidyShelves is tenant-centric. Identities from Cognito/Entra/Okta are front doors into tenants, but the source of truth for users, subscriptions, devices and invitations lives in our DB.
🏠 Tenants & Users
- Each tenant corresponds to a household or small business.
- First user → creates a new tenant and becomes the Owner.
- Additional users join via invitation deep links into the same tenant.
- user_identities map IdP identity → internal
user_id.
🔄 Sync & Change Log
- Single endpoint:
POST /data/entities/synchandles push + pull. - Server applies mutations, records changes in
entity_change_logwith version. - Clients track
lastPulledVersionper device. - PostgreSQL functions implement upsert, move, soft-delete, restore, purge.
💳 Subscriptions & Limits
- Plans define limits: items, users, devices, photos per item, and sync cadence.
- Sync cadence is expressed as
syncIntervalMinutesin backend config (e.g., Manual / 6 hrs / 3 hrs / 1 hr / 10 min / Real-time). - Config service exposes effective subscription config to data/file services and iOS.
- iOS uses cadence as a hint for when to auto-attempt sync (offline-first preserved).
📨 Invitations & Onboarding
- Owners send invites via user_invites table + email deep links.
- Invited user taps deep link → IdP login → backend
GetOrCreateUserAsync. - Invite ties new user to the existing tenant (subject to UsersLimit).
- Flow is similar to Netflix / household invites, but tenant-scoped.
🗺 Server-First Implementation Roadmap
The roadmap is intentionally server-first: complete all core backend building blocks and exercise them via Postman before wiring the iOS app into sync, subscriptions, and invites.
- Core Data + UI stable with zero network dependency.
- EntitiesStore supports create/update/move/soft-delete/restore.
- Recently Deleted, move picker, detail view, and trash semantics in place.
Already Implemented
POST /data/entities/syncimplemented.- Idempotent mutations via
entity_mutation_applied. - PostgreSQL:
entity_upsert,soft_delete_entity,restore_deleted_group,purge_deleted_group,move_entity_tree. - Changefeed via
entity_change_logwith monotonic version. - Pending micro-fix: treat “delete non-existent entity” as true no-op and skip change log.
Already Implemented (with minor fix remaining)
- Postman collection for all mutation types (Upsert, Move, SoftDelete, Restore, Purge).
- Scenarios: create→delete→restore, move subtrees, pagination with
HasMore, idempotent replays. - Validate delete-before-create classical bug is handled gracefully.
Already Implemented
- Create subscription tables:
subscription_plan,tenant_subscription. - Define limits:
maxItems,maxUsers,maxDevices,maxPhotosPerItem. - Add cadence:
syncIntervalMinutestosubscription_plan(null or 0 = manual-only). - Add trial metadata to
tenant_subscription:trialDays,trialEndsAt,status(trialing,active,canceled). - Implement
GET /config/subscriptionreturning effective config per tenant, includingsyncIntervalMinutesand current trial state. - Seed initial plans (Starter, Solo, Essentials, Pro, Business, Enterprise).
Pending
- Postman: verify plan configurations for multiple tenants.
- Simulate upgrades/downgrades and check resulting config.
- Test behavior when tenant has no explicit subscription row (fallback to default plan).
Pending
- Implement
tenants,users,user_identitiestables. - Create
GetOrCreateUserAsync(ClaimsPrincipal, invitationId?). - On first login (no invite), auto-create tenant + owner user on the Starter (free) plan.
- When a paid plan (Solo, Essentials, Pro, Business) is selected for the first time, create a
tenant_subscriptionrow withstatus = "trialing"andtrialEndsAt = now + trialDays. - After trial expiry, automatically transition to
status = "active"(or enforce downgrade) based on billing outcome.
Pending
- Postman tests: first login → tenant + owner bootstrap.
- Ensure user_identity correctly maps provider subject to internal IDs.
- Validate behavior on repeated logins and crossing tenants.
Pending
- Implement
user_invitestable with expiry, status, and invited email. POST /auth/invite(owner-only) → returns deep link (e.g.,tidyshelves://invite?token=...).POST /auth/bootstrapaccepts optionalX-Invite-Idor invite token and attaches the user to the correct tenant.- Enforce
maxUsersfrom subscription before accepting invite (trialing or active). - Ensure that joining during a trial keeps the tenant on the same
trialEndsAtdate (no per-user trial resets).
Pending (critical multi-user onboarding piece)
- Generate invites as an owner via Postman.
- Simulate “invite acceptance” by calling bootstrap endpoints with tokens.
- Test expired/used/invalid invite error paths.
- Test hitting UsersLimit and correct error responses.
Pending
- Implement
user_devicestable. POST /device/registerto associateuser_id+deviceId.- Validate against subscription config
maxDevices. - Prepare response contract for iOS (allowed / over-limit).
Pending
- Register multiple devices via Postman and verify limit behavior.
- Confirm correct responses for over-limit scenarios.
Pending
- Introduce
CD_SyncOutbox+CD_SyncStateinto Core Data. - Wrap all local mutations with outbox enqueueing.
- Respect
syncIntervalMinuteswhen scheduling background/foreground auto-sync attempts. - Ensure background-safe Core Data writes and consistent IDs.
Pending (first iOS sync phase)
- Push outbox →
/data/entities/sync, handle mutation results. - Pull changefeed → apply via background context, update
lastPulledVersion. - Trigger sync if
lastSuccessfulSyncAtexceeds cadence threshold (based on plan). - Triggers: manual, app foreground, network-back-online, debounced local changes.
Pending
- Fetch
/config/subscriptionon app launch / refresh. - Enforce local item limits even when offline (friendly banners).
- Surface device over-limit and UsersLimit errors in understandable UI.
Pending
- Instrument sync timings, failure categories, and retries.
- Measure subscription limit hits and invite flows for product insights.
- Polish UX flows and documentation.
Pending