14 KiB
Fchati — Current State & Tasks
Important: The Mac app must be built entirely without opening Xcode. Use
Package.swift(Swift Package Manager) as the project structure. All tasks are agent-ready: copy the prompt and send it directly to your coding agent.
Rules for every agent
These rules apply to every agent that works on this project, without exception.
1. No Arabic anywhere. All code, comments, string literals, log messages, error messages, and documentation files must be in English only.
2. Mark your task as done.
When you finish a task, find it in this file and change its status line from [ ] to [x] and add today's date. Example:
- [x] TASK-02 — KeychainStore (done 2026-07-26)
This tells the next agent not to redo your work.
3. Document what you did.
Update IMPLEMENTATION_STATUS.md with: what files you created or changed, what was verified (tests, build), and anything you found missing or left incomplete. Write in English. This file is how the next session picks up exactly where you left off.
4. Verify before finishing.
Run swift build from FchatiApp/ before declaring the task complete. If it does not compile, fix it. Do not leave broken code.
5. Do not touch completed tasks.
If a task is marked [x], do not modify those files unless explicitly instructed.
Architecture Decisions (source of truth)
These are decisions made during planning — do not change without revisiting the reasoning.
Transport: WebSocket (WSS) over Traefik. Traefik handles TLS via Let's Encrypt. The app uses URLSessionWebSocketTask — no custom TCP stack, no certificate pinning, no OpenSSL.
Server role: The relay is a dumb pipe. It does not store conversation history. It only holds undelivered messages temporarily (offline queue). Once delivered, messages are deleted from the server.
Conversation history: Stored locally on each device only. The app saves every message it sends or receives to a local JSON file. This is the permanent record — the server is not the archive.
Offline delivery: Messages sent to an offline peer are queued on disk on the server (not in RAM). When the peer reconnects, all queued messages are flushed immediately. Queue TTL: 7 days. Queue is disk-backed so it survives server restarts.
Pairing sessions: Also persisted on disk. Tokens survive server restarts — users never need to re-pair after a server update.
Read receipts: When a peer opens and reads messages, the app sends { type: "read", messageIDs: [...] }. The server forwards this to the sender (or queues it if offline). The sender's app then shows "read" status on those messages.
File storage: Uploaded files (images, voice, documents) live on the server at /data/files. Text message JSON is negligible in size (a year of heavy messaging ≈ tens of MB). Files are the real storage concern — photos/voice accumulate at ~1–2 GB/month with heavy use.
File cleanup: Files older than 30 days should be auto-deleted by the server. A manual "Clean now" button will also be available in Settings. Text messages on the device are never auto-deleted.
Server requirements: A $5/month VPS (1 vCPU, 1 GB RAM) is more than sufficient for a small group of friends. The relay holds connections open but does no heavy processing.
No iCloud, no third-party services: Everything flows through the self-hosted relay. Apple has no access to messages.
Mac app distribution: Not via App Store. Distribution options TBD (direct .app download, notarization, or TestFlight).
Current State
Done
| Part | Details |
|---|---|
relay-server/src/index.js |
Node.js relay — Pairing, WebSocket, offline queue (disk-backed), read receipts, file upload/download |
relay-server/Dockerfile |
Multi-stage Alpine build |
relay-server/docker-compose.yml |
Traefik labels for fchati.diyaa.de, three persistent volumes (files, queue, pairing) |
relay-server/.env.example |
Environment variable template |
| Git repo | git.mohfarawati.de/diyaa/fchaty |
Not built yet
The Mac app (FchatiApp/) does not exist yet.
Pending server tasks
| Task | Details |
|---|---|
| File auto-cleanup | ✅ Done 2026-07-26 — deletes files older than FILE_TTL_DAYS on startup and daily. |
| Manual cleanup endpoint | ✅ Done 2026-07-26 — protected POST /admin/cleanup deletes files older than a requested number of days. |
| Deploy to server | git pull && docker compose up -d --build on the production server. Not done yet — server is still being set up. |
Target App Structure
f-chaty-native-new/
├── relay-server/ ✅ done
└── FchatiApp/
├── Package.swift no Xcode — SPM only
├── Sources/
│ └── FchatiApp/
│ ├── FchatiApp.swift
│ ├── Session/
│ │ └── AppSession.swift
│ ├── Networking/
│ │ ├── RelayAPI.swift
│ │ ├── WSClient.swift
│ │ └── MessageRouter.swift
│ ├── Storage/
│ │ ├── KeychainStore.swift
│ │ └── MessageStore.swift
│ └── Features/
│ ├── Chat/
│ │ ├── ChatView.swift
│ │ ├── ChatViewModel.swift
│ │ └── MessageBubble.swift
│ ├── Pairing/
│ │ ├── PairingView.swift
│ │ └── PairingViewModel.swift
│ └── Settings/
│ └── SettingsView.swift
└── FchatiApp.entitlements
Tasks
Each task is fully independent. Send any one to a coding agent without context from the others.
TASK-01 — Swift Package + App Shell ✅ done 2026-07-26
Completed. See IMPLEMENTATION_STATUS.md for details.
Post-review fixes: Added LSUIElement to Info.plist, applied entitlements in codesign script.
Requirements:
Create FchatiApp/Package.swift:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "FchatiApp",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "FchatiApp",
path: "Sources/FchatiApp"
)
]
)
Create FchatiApp/Sources/FchatiApp/FchatiApp.swift:
@mainSwiftUI App- Uses
MenuBarExtrawith.windowstyle - Menu bar icon:
Image(systemName: "message") - Popover width: 360pt, min height: 480pt
- A root view with three tabs: Chat, Pairing, Settings (segmented picker at top)
Create FchatiApp/FchatiApp.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
Verify: swift build succeeds from FchatiApp/ directory.
TASK-02 — KeychainStore ✅ done 2026-07-26
Reviewed: No issues. Clean Security.framework usage, correct access policy (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), clearAll() correctly preserves installationID.
TASK-03 — RelayAPI (HTTP) ✅ done 2026-07-26
Reviewed: Good. All endpoints correct, multipart/form-data properly formatted, error types match spec.
Note for future: uploadFile loads entire file into memory — acceptable for now, revisit if large video support is needed.
TASK-04 — WSClient (WebSocket) ✅ done 2026-07-26
Reviewed: Excellent. Reconnect backoff correct, auth timeout clean, race conditions protected via task identity.
Fix applied: Added messageIDs: [String]? and attachment: WSAttachment? to WSMessage — required for read receipts and file messages.
TASK-05 — AppSession (State Machine)
- TASK-05 — AppSession (State Machine) (done 2026-07-26)
Priority: Third — after TASK-02, TASK-03, TASK-04
File: FchatiApp/Sources/FchatiApp/Session/AppSession.swift
Depends on: KeychainStore, RelayAPI, WSClient
Requirements:
enum AppState: Equatable {
case unpaired
case connecting
case connected(peerID: String, peerName: String)
case error(String)
}
@MainActor
final class AppSession: ObservableObject {
static let shared = AppSession()
@Published var state: AppState = .unpaired
@Published var messages: [ChatMessage] = []
// Pairing
func createPairingCode(displayName: String) async throws -> String // returns code
func joinWithCode(_ code: String, displayName: String) async throws
// Messaging
func sendText(_ body: String) async throws
func sendFile(url: URL) async throws
// Lifecycle
func unpair()
}
On init: read token from KeychainStore. If present, attempt WSClient.connect immediately.
On incoming chat.message: append to messages and trigger a UNUserNotification if app is in background.
TASK-06 — PairingView
- TASK-06 — PairingView (done 2026-07-26)
Priority: Fourth — after TASK-05
Files: Features/Pairing/PairingView.swift, PairingViewModel.swift
Requirements:
Two modes switchable via a segmented picker: "Create Code" / "Enter Code".
Create Code mode:
- Text field for display name
- "Create Code" button → calls
AppSession.shared.createPairingCode - Shows the returned code (e.g.
FCHT-AB3K7Q) in a monospaced font with a Copy button - Shows "Code expires in 5 minutes"
- Shows a spinner while waiting for the other peer to join
Enter Code mode:
- Text field for display name
- Text field for code — auto-uppercases, accepts with or without
FCHT-prefix - "Connect" button → calls
AppSession.shared.joinWithCode - Shows inline error if code is wrong or expired
Both modes observe AppSession.shared.state and navigate to ChatView when state becomes .connected.
TASK-07 — ChatView
- TASK-07 — ChatView (done 2026-07-26)
Priority: Fourth (parallel with TASK-06)
Files: Features/Chat/ChatView.swift, ChatViewModel.swift, MessageBubble.swift
Requirements:
MessageBubble:
- Sent messages: right-aligned, blue bubble
- Received messages: left-aligned, gray bubble
- Render message body using
AttributedString(markdown:)for Markdown support - Show timestamp below bubble
ChatView:
ScrollViewReaderwith auto-scroll to latest message- Message list from
AppSession.shared.messages - Text input bar at bottom + Send button (also triggers on Return key)
- Paperclip button 📎 opens
NSOpenPanelfor file selection → callsAppSession.shared.sendFile - Microphone button 🎙 — hold to record (
AVAudioRecorder,.m4a), release to send as file
TASK-08 — MessageStore (Local Persistence)
- TASK-08 — MessageStore (Local Persistence) (done 2026-07-26)
Priority: Fourth (parallel)
File: FchatiApp/Sources/FchatiApp/Storage/MessageStore.swift
Independent: Yes
Requirements:
struct ChatMessage: Codable, Identifiable, Equatable {
let id: String
let from: String
let fromName: String
let body: String
let sentAt: Date
let attachment: AttachmentInfo?
var isRead: Bool
}
struct AttachmentInfo: Codable, Equatable {
let fileID: String
let name: String
let size: Int
}
actor MessageStore {
static let shared: MessageStore
func save(_ message: ChatMessage) throws
func loadAll() throws -> [ChatMessage]
func markRead(id: String) throws
var unreadCount: Int { get async }
}
Storage: JSON file at FileManager.default.urls(for: .applicationSupportDirectory, ...).first!.appendingPathComponent("de.diyaa.fchati/messages.json").
Create the directory if it does not exist.
TASK-09 — SettingsView
- TASK-09 — SettingsView (done 2026-07-26)
Priority: Fifth
File: Features/Settings/SettingsView.swift
Requirements:
Simple settings screen:
- Display current user name (read from
KeychainStore.peerName) with an Edit button - Show connection status: "Connected to [peer name]" or "Not connected"
- "Unpair" button → calls
AppSession.shared.unpair()and clears Keychain - App version at the bottom (read from
Bundle.main.infoDictionary["CFBundleShortVersionString"])
TASK-10 — New Message Notifications
- TASK-10 — New Message Notifications (done 2026-07-26)
Priority: Fifth (independent)
File: FchatiApp/Sources/FchatiApp/Notifications/NotificationManager.swift
Requirements:
final class NotificationManager {
static let shared = NotificationManager()
func requestPermission() async
func notify(from senderName: String, body: String)
}
requestPermission()called once on first launchnotifyfires aUNUserNotificationRequestwith title = sender name, body = first 100 chars of message body- Do not send a notification if the menu bar popover is currently visible
How to Send a Task to a Coding Agent
Copy the block below, fill in the task number and paste it to your agent:
Context:
- Repo: https://git.mohfarawati.de/diyaa/fchaty.git
- App folder: FchatiApp/ (Swift Package, no Xcode, SPM only)
- macOS 14+, Swift 5.9+, no third-party libraries
- Relay server is already running at https://fchati.diyaa.de
Task: [paste the full task section here, e.g. TASK-04]
After writing the code:
1. Make sure it compiles: run `swift build` from the FchatiApp/ directory
2. Fix any compiler errors before finishing
3. Do not open Xcode or create any .xcodeproj or .xcworkspace files