The user's idea (2026-06-17, after the d6879b4 buy-in fix). This
is the user's own phrasing of the design — captured verbatim, not
paraphrased.
with the join code, what is a way the members that have already
inputted their join code into the app can get remembered and
aren't prompted to re-enter the code if they are re-logging in
to the app.
I had an idea that putting the join code after the sign in with
apple could work. and if the member that is sign in with apple
has already a code join in the database, the UI doesn't prompt
them to re-enter it. but if the members that has signed in with
apple doesn't have a associated join code inputted, then the
UI will prompt for one—with the app remaining lock like a
vault until the join code/invite code has been entered. Also
it probably goes without saying that the join code the new
members enter must match the ones the admins create.
Current state (after 25a58d2):
pendingInvite != nilProposed state (this idea):
currentMember.inviteKey AuthService.validateInvitation query against felty_invite_codes
user tapping https://feltfaction.app/invite/<code> from iMessage lands in the app, signs in with Apple, and the code is pre-filled in the vault (or auto-consumed if they have a session). One tap, one SiwA, unlocked.
the Supabase session expires (the standard iOS background lifecycle eventually causes this), the user re-signs in and goes straight to the app. No "enter your code again."
the UI.** The vault is the gate; the gate is a screen, not a pre-screen. The invitation code is what unlocks the house, and the DB remembers who has unlocked it.
→ validateInviteCode → pendingInvite is set. After sign-in, the pendingInvite is consumed in InvitationGateView. One path, two surfaces (pre-sign-in and post-sign-in), one source of truth (felty_invite_codes).
// AppState.AuthState
enum AuthState: Equatable {
case unknown
case signedOut // no user session
case signedIn(Role) // session exists, invite_key is set
case vaultLocked(Role) // session exists, no invite_key
// → InvitationGateView
}
AppState.refreshAuth routes to .signedIn or .vaultLocked based on currentMember?.inviteKey != nil. The RootView routes each case to the right screen.
AppState.swift — add vaultLocked(Role), update refreshAuth, add completeVaultUnlock(code:) async method
AuthService.swift — add linkInvitationToMember(_:invite:) (separate from the existing consumeInvitation which is the create-from-OAuth path)
Features/Auth/InvitationGateView.swift (new) — the vault UIApp/RootView.swift — route to InvitationGateView for vaultLocked, simplify WelcomeView (no more pre-sign-in text field)
InvitationURLParserTests still apply; the new vaultLocked state needs a state-machine test (deferred: AppState is @MainActor and requires a Supabase mock, neither of which the test target has)
invite_key is set → MainTabView. No prompt. ✓
InvitationGateView. User types code. ✓
pendingInvite → SiwA → InvitationGateView with code pre-filled. One tap to unlock. ✓
informational; signedIn → MainTabView. (Future: show a "you're already a member" confirmation?)
created** → validateInvitation throws AuthError.invitationInvalid; InvitationGateView shows the error; user can retry. ✓
("This invitation has expired. Ask the admin for a new one."). ✓
whichever signs in first unlocks; the second lands in signedIn because the DB already has invite_key. ✓
This idea is the v8 design's natural endpoint. v8 said: "the user shouldn't have to copy anything." v8 solved the distribution (the invite is a Universal Link, the user taps once). This idea solves the entry (the user lands where they need to, gated only on what the DB knows about them). The two together: tap once → app opens → sign in once → unlocked.
The v9 pivot moved invite generation from the Telegram bot (Felty) to the iOS admin surface (GenerateInviteView). This idea is the consumption side of the same move: the join code is now consumed by the iOS app, not by the bot. The distribution + consumption loop is fully native to the iOS app.
This is a substantial refactor (AppState's auth state machine, RootView routing, a new view, a new AuthService method, and the simplification of WelcomeView). Capturing the user's idea verbatim to gbrain first, per the signal-detector skill's "capture exact phrasing" rule, so future sessions can find both the design and the reasoning. The implementation follows in the next commit.
<!--- gbrain:takes:begin --> | # | claim | kind | who | weight | since | source | |---|-------|------|-----|--------|-------|--------| | 1 | The app should move join-code gating after Sign in with Apple and route users by whether currentMember.inviteKey is set. | take | brain | 0.9 | 2026-07-19 | propose_takes#1135 | | 2 | The 'by invitation only' promise is preserved by the DB layer (currentMember.inviteKey check), not the pre-sign-in UI. | take | brain | 0.8 | 2026-07-19 | propose_takes#2326 | <!--- gbrain:takes:end -->
Published and managed by TARS, an AI co-author built on Nathan's gbrain.