Skip to main content

Overview

Chapter uses Supabase as its primary backend service, providing:
  • PostgreSQL Database: Relational data storage for users, courses, accommodations, posts, and more
  • Real-time Subscriptions: Live updates via WebSocket channels
  • Authentication: Email-based magic link authentication with OTP verification
  • Storage: Object storage for images, videos, and user-generated content
  • Row Level Security (RLS): Database-level security policies

Client Configuration

The Supabase client is initialized in AuthManager and shared throughout the app:
AuthManager.swift:153-170
init() {
    supabaseURL = "https://tlnelgccwzoeszfejgls.supabase.co"
    supabaseAPIKey = Bundle.main.object(forInfoDictionaryKey: "SUPABASE_API_KEY") as? String
    
    guard let supabaseURLString = supabaseURL,
          let supabaseKey = supabaseAPIKey,
          let supabaseURL = URL(string: supabaseURLString) else {
        fatalError("Missing SUPABASE_URL or SUPABASE_API_KEY in Info.plist")
    }
    
    supabase = SupabaseClient(supabaseURL: supabaseURL, supabaseKey: supabaseKey)
    storageService = StorageService(supabaseClient: supabase)
}

Environment Configuration

The Supabase API key is stored in Info.plist as SUPABASE_API_KEY for security. Never hardcode credentials in source files.
The URL is currently hardcoded in source but should be moved to Info.plist for production builds.

Database Operations

Querying Tables

Chapter uses the Supabase Swift SDK’s query builder pattern:
// Simple query
let users: [GroupUser] = try await supabase
    .from("users_v2")
    .select("*")
    .eq("id", value: userID)
    .execute()
    .value

// Query with filters and ordering
let posts: [Post] = try await supabase
    .from("posts")
    .select("*, creator:users_v2(*)")
    .eq("group_id", value: groupID)
    .order("created_at", ascending: false)
    .limit(20)
    .execute()
    .value

RPC Functions

For complex queries, Chapter uses PostgreSQL functions via RPC:
SupabaseMethods.swift:55-78
static func getMultipleCoursesV2(ids: [UUID], userLat: Double?, userLon: Double?) async throws -> [CourseV2Short] {
    let supabase = AuthManager.shared.supabase
    do {
        struct CourseParams: Codable {
            let courseIds: [String]
            let user_lat: Double?
            let user_lon: Double?
        }
        let response = try await supabase.rpc(
            "get_multiple_courses", 
            params: ["params": CourseParams(
                courseIds: ids.map({$0.uuidString}), 
                user_lat: userLat, 
                user_lon: userLon
            )]
        ).execute()
        
        let courses = try JSONDecoder().decode([CourseV2Short].self, from: response.data)
        return courses
    } catch {
        ErrorService.shared.showError(title: "Error Fetching Courses", description: error.localizedDescription)
        throw error
    }
}

Insert Operations

PostsService.swift:131-157
private func createPostAndComments(post: Post) async throws {
    let postForInsertion = InsertPost(
        id: post.id,
        title: post.title,
        type: post.type,
        content: post.content,
        imageURLs: post.imageURLs,
        createdAt: post.createdAt,
        createdBy: post.creatorID,
        groupID: post.groupID,
        likes: post.likes,
        location: post.location,
        links: post.links,
        visibility: post.visibility,
        source: post.source
    )
    
    let postResponse = try await supabase
        .from("posts")
        .insert(postForInsertion)
        .execute()
    
    guard postResponse.status == 200 || postResponse.status == 201 else {
        throw NSError(
            domain: "PostsService",
            code: postResponse.status,
            userInfo: [NSLocalizedDescriptionKey: "Error creating post. Status: \(postResponse.status)"]
        )
    }
}

Update Operations

try await supabase
    .from("users_v2")
    .update(["first_name": firstName])
    .eq("id", value: currentUserId)
    .execute()

Delete Operations

PostsService.swift:236-240
try await supabase
    .from("post_likes")
    .delete()
    .eq("user_id", value: user.id)
    .eq("post_id", value: post.id)
    .execute()

Date Handling

Supabase returns dates in ISO8601 format. Chapter uses custom date decoders to handle fractional seconds:
SupabaseMethods.swift:25-46
func setupUserDecoder() {
    SupabaseMethods.userDecoder.dateDecodingStrategy = .custom { decoder in
        let container = try decoder.singleValueContainer()
        let dateString = try container.decode(String.self)
        
        // Attempt first with fractional seconds
        let isoFormatterWithFractional = ISO8601DateFormatter()
        isoFormatterWithFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds, .withTimeZone]
        if let date = isoFormatterWithFractional.date(from: dateString) {
            return date
        }
        
        // Attempt without fractional seconds
        let isoFormatterWithoutFractional = ISO8601DateFormatter()
        isoFormatterWithoutFractional.formatOptions = [.withInternetDateTime, .withTimeZone]
        if let date = isoFormatterWithoutFractional.date(from: dateString) {
            return date
        }
        
        throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
    }
}
Use the shared SupabaseMethods.userDecoder for consistent date parsing across the app.

Error Handling

Chapter implements comprehensive error handling for database operations:
do {
    let response = try await supabase
        .from("users_v2")
        .select("*")
        .execute()
    
    guard response.status == 200 else {
        throw NSError(
            domain: "SupabaseError", 
            code: response.status, 
            userInfo: [NSLocalizedDescriptionKey: "Failed to fetch users. Status: \(response.status)"]
        )
    }
    
    let users = try decoder.decode([GroupUser].self, from: response.data)
    return users
    
} catch {
    ErrorService.shared.showError(
        title: "Database Error",
        description: error.localizedDescription
    )
    throw error
}

SupabaseMethods Helper Class

Chapter centralizes common database operations in SupabaseMethods.swift. This singleton provides reusable methods for fetching entities:
// Fetch multiple courses
let courses = try await SupabaseMethods.getMultipleCoursesV2(
    ids: courseIDs,
    userLat: userLocation?.latitude,
    userLon: userLocation?.longitude
)

// Fetch university details
let university = try await SupabaseMethods.getUniV2(id: universityID)

// Fetch user follows
let followedUsers = try await SupabaseMethods.getUserFollows()

// Fetch group details
let group = try await SupabaseMethods.getGroupDetails(group: groupID)

Benefits of Centralization

  • Consistency: All database calls use the same error handling and decoding logic
  • Reusability: Common queries are written once and used everywhere
  • Maintenance: Database schema changes require updates in one place
  • Testing: Easier to mock and test database interactions

Best Practices

1. Always Handle Errors

Never silently fail on database operations:
// ✅ Good
try await supabase.from("posts").insert(post).execute()

// ❌ Bad
try? await supabase.from("posts").insert(post).execute()

2. Use Codable Models

Define Swift structs that match your database schema:
struct GroupUser: Codable {
    let id: UUID
    let firstName: String
    let lastName: String
    let email: String
    
    enum CodingKeys: String, CodingKey {
        case id
        case firstName = "first_name"
        case lastName = "last_name"
        case email
    }
}

3. Check Response Status

Always validate the HTTP response status:
let response = try await supabase.from("table").select().execute()
guard response.status == 200 else {
    throw NSError(domain: "SupabaseError", code: response.status)
}

4. Use RPC for Complex Queries

PostgreSQL functions provide better performance for:
  • Multi-table joins
  • Computed fields
  • Complex filtering logic
  • Aggregations

5. Implement Retry Logic

For critical operations, implement exponential backoff:
func fetchWithRetry<T>(_ operation: () async throws -> T, maxAttempts: Int = 3) async throws -> T {
    var lastError: Error?
    
    for attempt in 1...maxAttempts {
        do {
            return try await operation()
        } catch {
            lastError = error
            if attempt < maxAttempts {
                try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
            }
        }
    }
    
    throw lastError!
}

Authentication

Learn about the auth flow

Real-time Features

Implement live updates

Data Services

Service layer architecture

Build docs developers (and LLMs) love