Skip to content

1amageek/FirebaseAPI

Repository files navigation

FirebaseAPI

FirebaseAPI is a Swift package for server-side Cloud Firestore access over gRPC. It provides a Firebase-style public API for documents, queries, transactions, batches, listeners, Codable models, native geohash queries, Firestore Pipeline, and MongoDB-compatible query documents.

The recommended 2.x Admin entry point is:

import FirestoreAdmin

let firestore = try Firestore.applicationDefault()

Requirements

Requirement Version
Swift 6.2+
macOS 15.0+
iOS 18.0+
watchOS 11.0+
tvOS 18.0+
visionOS 2.0+

Products

FirebaseAPI intentionally exposes three SwiftPM products.

Product Use this when Public shape
FirestoreAdmin Building a new server-side Firestore service Imports the Firestore facade, Admin builders, Codable helpers, Auth, Core, Pipeline, RuntimeConfig, and native GeoQuery
FirestoreMongo Building MongoDB-compatible query/index documents Imports Mongo-compatible GeoJSON, $near, and 2dsphere document builders
FirestoreAPI Keeping an existing all-in-one import working Re-exports Admin, Core, Codable, Pipeline, Auth, GeoQuery, and Mongo-compatible helpers
flowchart LR
  A["Application"] --> B{"Needed surface"}
  B --> C["FirestoreAdmin"]
  B --> D["FirestoreMongo"]
  B --> E["FirestoreAPI"]
  C --> F["Firestore facade"]
  D --> G["Mongo-compatible query documents"]
  E --> H["Compatibility all-in-one import"]
Loading

FirestoreAdmin is the product/module name. Firestore is the public runtime facade type. The old FirestoreAdmin facade type is not part of the 2.x API.

Installation

Add the package dependency:

dependencies: [
    .package(url: "https://github.com/1amageek/FirebaseAPI.git", from: "2.0.0")
]

Add the product you need:

.target(
    name: "YourService",
    dependencies: [
        .product(name: "FirestoreAdmin", package: "FirebaseAPI")
    ]
)

Add FirestoreMongo only for MongoDB-compatible query document construction:

.product(name: "FirestoreMongo", package: "FirebaseAPI")

Quick Start

import FirestoreAdmin

let credentials = try ServiceAccountCredentials.load(from: serviceAccountJSONURL)
let firestore = try Firestore(credentials: credentials)

let city = try firestore.collection("cities").document("tokyo")

try await city.setData([
    "name": "Tokyo",
    "country": "JP",
    "population": 14_000_000,
    "updatedAt": FieldValue.serverTimestamp()
])

let snapshot = try await city.getDocument()
print(snapshot.data() ?? [:])

await firestore.shutdown()

Call shutdown() during service teardown so the gRPC client stops accepting new RPCs and drains in-flight requests.

Authentication

Service Account JSON

import FirestoreAdmin

let credentials = try ServiceAccountCredentials.load(from: serviceAccountJSONURL)
let firestore = try Firestore(credentials: credentials)

Application Default Credentials

Use this when the project ID is available synchronously from the environment or well-known ADC file:

let firestore = try Firestore.applicationDefault()

Use this on Google Cloud runtimes where the metadata server may provide the project ID asynchronously:

let firestore = try await Firestore.applicationDefaultResolvingProjectID()

Firestore Emulator

let firestore = try Firestore.emulator(
    projectId: "demo-project",
    host: "127.0.0.1",
    port: 8080
)

Custom Transport

Host-provided transports are useful for Wasm and embedded runtimes.

let firestore = Firestore.admin(
    projectId: "demo-project",
    transport: transport,
    settings: .hostManagedAuthentication()
)

Documents and Codable

import FirestoreAdmin

struct City: Codable {
    @DocumentID var id: String
    @ReferencePath var path: String
    @ServerTimestamp var updatedAt: Timestamp?

    var name: String
    var country: String
    var population: Int
}

let cities = try firestore.collection("cities")
let tokyo = try cities.document("tokyo")

try await tokyo.setData([
    "name": "Tokyo",
    "country": "JP",
    "population": 14_000_000
])

try await tokyo.updateData([
    "population": 14_100_000,
    "updatedAt": FieldValue.serverTimestamp()
])

let city = try await tokyo.getDocument(as: City.self)
let allCities = try await cities.getDocuments(as: City.self)

Supported convenience wrappers include @DocumentID, @ReferencePath, @ExplicitNull, and @ServerTimestamp.

Queries

let cities = try firestore.collection("cities")

let snapshot = try await cities
    .whereField("country", isEqualTo: "JP")
    .whereField("population", isGreaterThanOrEqualTo: 1_000_000)
    .order(by: "population", descending: true)
    .limit(to: 20)
    .getDocuments()

for document in snapshot.documents {
    print(document.documentID, document.data())
}

Composite filters are built with Filter:

let filter = Filter.orFilter(with: [
    Filter.filter(whereField: "country", isEqualTo: "JP"),
    Filter.filter(whereField: "country", isEqualTo: "US")
])

let snapshot = try await cities
    .whereFilter(filter)
    .order(by: "name")
    .getDocuments()

Aggregations and Explain are first-class query operations:

let count = try await cities.count()
let averagePopulation = try await cities.average("population")
let plan = try await cities.explain(options: .planOnly)

Transactions, Batches, and Bulk Writes

Use transactions when reads and writes must be coordinated:

let updatedPopulation = try await firestore.runTransaction { transaction in
    let reference = try firestore.document("cities/tokyo")
    let snapshot = try await transaction.getDocument(reference)
    let population = snapshot.data()?["population"] as? Int ?? 0
    let nextPopulation = population + 1

    transaction.updateData(
        ["population": nextPopulation],
        forDocument: reference
    )

    return nextPopulation
}

Use batch() for atomic write groups:

let batch = firestore.batch()

batch.setData(["name": "Tokyo"], forDocument: try firestore.document("cities/tokyo"))
batch.setData(["name": "Osaka"], forDocument: try firestore.document("cities/osaka"))

try await batch.commit()

Use bulkWriter() for non-atomic high-throughput writes backed by Firestore BatchWrite:

let writer = firestore.bulkWriter()

writer.setData(["name": "Tokyo"], forDocument: try firestore.document("cities/tokyo"))
writer.setData(["name": "Osaka"], forDocument: try firestore.document("cities/osaka"))

let result = try await writer.flush(labels: ["job": "city-import"])
print(result.results)
API Firestore RPC behavior Atomic
DocumentReference.setData, updateData, delete Commit Yes, per call
firestore.batch().commit() Commit Yes
firestore.runTransaction BeginTransaction, reads, Commit, optional Rollback Yes
firestore.bulkWriter().flush() BatchWrite No

Listeners

Server-side listeners use AsyncSequence.

let reference = try firestore.document("cities/tokyo")

for try await snapshot in reference.snapshots {
    if snapshot.exists {
        print(snapshot.data() ?? [:])
    }
}
let query = try firestore.collection("cities")
    .whereField("country", isEqualTo: "JP")

for try await snapshot in query.snapshots {
    print(snapshot.documents.count)
}

Cancel the surrounding task to stop listening.

Native GeoQuery

Native geo queries use a geohash field plus a GeoPoint field. FirebaseAPI builds the geohash range queries and filters the returned documents by exact distance.

let places = try firestore.collection("places")
let location = GeoPoint(latitude: 35.681236, longitude: 139.767125)
let reference = try places.document("tokyo-station")

try await reference.setData([
    "name": "Tokyo Station",
    "geohash": try FirestoreGeoHash.encode(location),
    "location": location
])

let results = try await places
    .geoQuery(
        center: location,
        radiusInMeters: 1_000,
        geohashField: "geohash",
        locationField: "location"
    )
    .getDocuments()

for result in results {
    print(result.document.documentID, result.distanceInMeters)
}

Native GeoQuery is separate from MongoDB-compatible $near query document construction.

Vector Search

let snapshot = try await firestore.collection("articles")
    .findNearest(
        vectorField: "embedding",
        queryVector: .vector([0.12, 0.42, 0.86]),
        limit: 10,
        distanceMeasure: .cosine,
        distanceResultField: "distance"
    )
    .getDocuments()

Firestore Pipeline

Firestore Pipeline is exposed through a typed builder and executed by the Admin runtime.

let pipeline = firestore.pipeline()
    .collection("cities")
    .where(PipelineValue.field("population").greaterThan(1_000_000))
    .sort([PipelineValue.field("population").descending()])
    .limit(10)

let rows = try await firestore.execute(pipeline)
let explanation = try await firestore.explain(pipeline)

Pipeline query construction is intentionally separate from normal Firestore Query construction and from MongoDB-compatible query documents.

MongoDB-Compatible Query Documents

FirestoreMongo builds MongoDB-compatible query/index documents. It does not execute them through the native Firestore gRPC Admin runtime.

import FirestoreMongo

let point = try FirestoreMongoGeoJSONPoint(
    longitude: 139.767125,
    latitude: 35.681236
)

let query = try FirestoreMongoGeoNearQuery(
    fieldPath: "location",
    point: point,
    maxDistanceMeters: 1_000
)

let index = try FirestoreMongoGeoIndex(fieldPath: "location")

print(query.document)
print(index.document)

Wasm Boundary

FirestoreAdmin and FirestoreAPI build for WASI with a host-provided GRPCCore.ClientTransport. Default Posix networking is not available in WASI, so runtime execution must enter through the custom transport initializer and host-managed authentication:

let firestore = Firestore.admin(
    projectId: "demo-project",
    transport: transport,
    settings: .hostManagedAuthentication()
)

See docs/FirestoreWasmBoundary.md for the full boundary contract.

Architecture

The public API keeps user-facing Firestore values separate from protobuf, generated gRPC stubs, and concrete transport execution.

flowchart LR
  A["Firestore facade"] --> B["Core references and queries"]
  A --> C["Admin write builders"]
  A --> D["Firestore Pipeline"]
  B --> E["RPC compilers"]
  C --> E
  D --> F["Pipeline RPC compiler"]
  E --> G["gRPC transport"]
  F --> G
  G --> H["Firestore API"]
Loading

Important design records:

Document Purpose
FirestoreAdminCompatibility.md SDK-compatible server-side API decisions
FirestoreModuleSeparationPlan.md Target and dependency boundaries
FirestoreMongoCompatibility.md Native vs Mongo-compatible geospatial boundary
FirestoreWasmBoundary.md Wasm build and host transport boundary
FirestoreAdminCompletionAudit.md Release readiness evidence

Development

Initialize submodules before regenerating protobuf sources:

git submodule update --init --recursive

Regenerate Firestore protobuf and gRPC stubs:

./scripts/generate-firestore-protos.sh

Build the public products:

swift build --product FirestoreAdmin
swift build --product FirestoreAPI
swift build --product FirestoreMongo

Run the test suite:

perl -e 'alarm shift; exec @ARGV' 90 xcodebuild -scheme FirebaseAPI-Package -destination 'platform=macOS' test

The current suite contains 407 tests across 19 suites.

Run the release-readiness gate:

bash scripts/check-release-readiness.sh

The release-readiness gate builds public products, runs the Swift Testing suite, dumps public symbol graphs, and rejects protobuf, gRPC transport, or internal query-planning symbols in public API.

Integration Tests

Firestore emulator tests are enabled when FIRESTORE_EMULATOR_HOST is set.

XDG_CONFIG_HOME=/tmp/firebase-cli-config \
firebase emulators:exec --only firestore --project firebase-api-emulator-test \
"perl -e 'alarm shift; exec @ARGV' 180 xcodebuild -scheme FirebaseAPI-Package -destination 'platform=macOS' test"

Optional emulator variables:

Variable Purpose
FIRESTORE_EMULATOR_PROJECT_ID Override the emulator project ID
FIRESTORE_EMULATOR_DATABASE_ID Override the emulator database ID
FIRESTORE_EMULATOR_PIPELINE_SMOKE=1 Enable opt-in Pipeline emulator smoke tests

Live Firestore smoke testing is opt-in. Application Default Credentials can come from GOOGLE_APPLICATION_CREDENTIALS, the gcloud well-known ADC file, or the Google Cloud metadata server.

FIRESTORE_LIVE_SMOKE=1 \
FIRESTORE_LIVE_PROJECT_ID=your-project-id \
bash scripts/run-live-firestore-smoke.sh

Use diagnostics-only mode to inspect credential and project-ID candidates without running production RPCs:

FIRESTORE_LIVE_SMOKE=1 \
FIRESTORE_LIVE_DIAGNOSTICS_ONLY=1 \
bash scripts/run-live-firestore-smoke.sh

Dependencies

Dependency Role
grpc-swift-2 gRPC core API
grpc-swift-nio-transport Default HTTP/2 client transport
grpc-swift-protobuf Protobuf serialization for gRPC
swift-protobuf Protocol Buffer runtime
swift-crypto Service account JWT signing
swift-log Logging

License

FirebaseAPI is available under the MIT License. See LICENSE.

About

Lightweight Cloud Firestore Client API using googleapis gRPC.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors