Technical documentation for the real-time community feed feature spanning the iOS client and Phoenix backend.
graph LR
subgraph iOS App
A[CommunityTabView] --> B[CommunityViewModel]
B --> C[CommunityChannelService]
B --> D[APIClient]
end
subgraph Phoenix Backend
E[CommunitySocket] --> F[CommunityChannel]
G[CommunityController] --> H[Community Context]
H --> I[PubSub]
I --> F
end
C <-->|WebSocket<br/>wss://.../community/websocket| E
D -->|REST API<br/>GET/POST /api/community/*| G
sequenceDiagram
participant User
participant CommunityTabView
participant CommunityViewModel
participant CommunityChannelService
participant Keychain
participant CommunitySocket
participant Accounts
participant CommunityChannel
participant PubSub
User->>CommunityTabView: Opens Community tab
CommunityTabView->>CommunityViewModel: .onAppear
CommunityViewModel->>CommunityChannelService: connect()
CommunityChannelService->>Keychain: Read API token
Keychain-->>CommunityChannelService: api_token
CommunityChannelService->>CommunitySocket: wss://{domain}/community/websocket?token={api_token}
Note over CommunitySocket: connect/3 callback
CommunitySocket->>CommunitySocket: SHA256 hash token
CommunitySocket->>Accounts: get_user_by_api_token(hashed_token)
Accounts-->>CommunitySocket: %User{id: user_id}
CommunitySocket->>CommunitySocket: assign(:user_id, user_id)
CommunitySocket-->>CommunityChannelService: Socket connected
CommunityChannelService->>CommunityChannel: join("community:feed")
Note over CommunityChannel: join/3 callback
CommunityChannel->>CommunityChannel: user_joined_community_by_id?(user_id)
CommunityChannel->>PubSub: subscribe("community:feed")
CommunityChannel-->>CommunityChannelService: {:ok, reply}
CommunityChannelService-->>CommunityViewModel: Connected & joined
sequenceDiagram
participant UserB as User B (Author)
participant APIClient as APIClient (User B)
participant CommunityController
participant Community as Community Context
participant PubSub
participant CommunityChannel
participant CommunityChannelService as CommunityChannelService (User A)
participant CommunityViewModel as CommunityViewModel (User A)
participant SwiftUI as SwiftUI (User A)
UserB->>APIClient: Create post
APIClient->>CommunityController: POST /api/community/posts
CommunityController->>Community: create_post(attrs)
Community->>Community: Insert post into DB
Community->>PubSub: broadcast("community:feed", {:post_created, post})
Community-->>CommunityController: {:ok, post}
CommunityController-->>APIClient: 201 Created (JSON)
PubSub->>CommunityChannel: handle_info({:post_created, post})
CommunityChannel->>CommunityChannel: Serialize post to JSON payload
CommunityChannel->>CommunityChannelService: push("new_post", payload)
CommunityChannelService->>CommunityChannelService: Decode JSON → CommunityPost
CommunityChannelService->>CommunityViewModel: onNewPost(post)
CommunityViewModel->>CommunityViewModel: Deduplicate by post.id
CommunityViewModel->>CommunityViewModel: Prepend post to posts array
CommunityViewModel->>SwiftUI: @Observable triggers re-render
SwiftUI->>SwiftUI: Feed updates with new post
graph LR
subgraph Phoenix PubSub Broadcasts
A[post_created] -->|"new_post"| W[WebSocket Push]
B[post_updated] -->|"post_updated"| W
C[post_deleted] -->|"post_deleted"| W
D[like_toggled] -->|"post_likes_updated"| W
E[comment_created<br/>comment_deleted] -->|"post_comments_updated"| W
end
| WebSocket Event | Trigger | Payload | Client Action |
|---|---|---|---|
new_post |
Post creation | Full serialized post | Prepend to feed |
post_updated |
Post edit | Full serialized post | Replace post in-place |
post_deleted |
Post deletion | {id: post_id} |
Remove post from feed |
post_likes_updated |
Like/unlike toggle | {id, likes_count, liked_by_user_ids} |
Update like state on post |
post_comments_updated |
Comment create/delete | {id, comments_count} |
Update comment count on post |
stateDiagram-v2
[*] --> Disconnected
Disconnected --> Connecting : Tab appears
Connecting --> Connected : Socket opened + channel joined
Connected --> Disconnected : Tab disappears
Connected --> Disconnected : App backgrounds
Disconnected --> Connecting : App foregrounds
Connected --> Reconnecting : Socket error / drop
Reconnecting --> Connected : Auto-reconnect success
Reconnecting --> Reconnecting : Retry with backoff
Connected --> Disconnected : User logs out
note right of Reconnecting
SwiftPhoenixClient handles
reconnection automatically
with exponential backoff
end note
note right of Disconnected
Channel reference is
cleaned up on disconnect
end note
| Event | Source | Action |
|---|---|---|
| Tab appears | .onAppear on CommunityTabView |
CommunityChannelService.connect() |
| Tab disappears | .onDisappear on CommunityTabView |
CommunityChannelService.disconnect() |
| App backgrounds | UIApplication.didEnterBackgroundNotification |
CommunityChannelService.disconnect() |
| App foregrounds | UIApplication.willEnterForegroundNotification |
CommunityChannelService.connect() |
| Socket error | SwiftPhoenixClient internal | Auto-reconnect with exponential backoff |
| Logout | Auth flow | CommunityChannelService.disconnect() |