Swift Vapor 4.x chat server with PostgreSQL, WebSockets, and a vanilla-JS web frontend.
swift build # compile
swift test # run all tests (requires a running PostgreSQL)
swift test --filter ChatTests # run a single test classTests use a live PostgreSQL connection — ensure the DB is reachable with the env vars below before running.
| Variable | Default | Notes |
|---|---|---|
DATABASE_URL |
— | Preferred (Heroku). Overrides all below |
DATABASE_HOST |
localhost |
|
DATABASE_PORT |
5432 |
|
DATABASE_USERNAME |
postgres |
|
DATABASE_PASSWORD |
— | |
DATABASE_NAME |
postgres |
|
TLS_CERT_PATH / TLS_KEY_PATH |
— | Optional TLS |
See Architecture notes for a full breakdown.
API reference: Users · Chats · Contacts · Files · WebSocket · JSON schemas
Layer responsibilities (never mix):
- Controllers (
Sources/App/Controllers/): extract request params → call service → encode response. No DB access. - Services (
Sources/App/Services/): Swift actors, business logic only. No Vapor/Request imports. - Repositories (
Sources/App/Repositories/): all database queries; always eager-load relations needed by callers. - Models (
Sources/App/Models/): Fluent models +Serializables.swiftfor request/response structs.
Key files:
Sources/App/CoreService.swift— central actor, lazily initialises all sub-services; owns WebSocketManager + NotificationManagerSources/App/Misc/Aliases.swift—ServiceError = Abort,UserID = Int,ChatID = UUID, etc.Sources/App/Misc/Utils.swift— customJSONEncoder/JSONDecoderthat use UNIX timestamps (not ISO-8601)Sources/App/Misc/JSON.swift—JSONtype alias ([String: Sendable]),JSONSerializableprotocol
- UNIX timestamps everywhere — the custom encoder/decoder in
Utils.swiftis wired globally; never use ISO-8601 dates in API responses. ServiceErroris justAbort; throw it from services with appropriate HTTP status codes.- SVG icons — all reusable icons live in
Public/app/js/svg-icons.js; add new icons there, not inline. - Frontend auth —
currentUserJSON (withsession.accessToken) is persisted inlocalStorage;api.jsreads it for every request. WebSocket auth uses?token=URL param. - Chat
participantsKey— sorted + hashed user IDs; used for dedup lookup of personal chats. - Soft deletes — messages use
deletedAt; hard deletes are not used for messages. - File uploads — files are uploaded independently first, then referenced by
MediaInfoin the message payload. Previews are generated server-side. ReadOnlyFileMiddleware— static files are served only for GET/HEAD; do not bypass this.
Base class: AppTestCase (in Tests/AppTests/Misc/TestUtils.swift).
// Typical test structure
func test_01_getSomething() async throws {
let current = try await service.seedCurrentUser() // creates user with Bearer token in app
let user = try await service.seedUsers(count: 1, namePrefix: "User", usernamePrefix: "user")[0]
// seed data, then:
try await asyncTest(.GET, "api/chats", headers: .none, afterResponse: { res in
XCTAssertEqual(res.status, .ok, res.body.string)
let result = try res.content.decode([ChatInfo].self)
// assertions
})
}- Test methods are numbered sequentially:
test_01_,test_02_, … service.seedCurrentUser()creates user #1 and sets the request auth header automatically.- Use
asyncTest(_:_:headers:beforeRequest:afterResponse:)— notapp.test(...)— to avoid Swift concurrency warnings. AppLiveTestCaseruns against a real live server; use for WebSocket/push tests.- Clean up uploaded files after tests:
service.removeFiles(for: resource).