Skip to content

Firestore

Firestore is a document database with no instance to size and no network to attach. Declare a database, bind it to your service, and read and write documents as plain JavaScript objects.

src/resources.ts
import * as GCP from "@alchemy.run/gcp";
export const Links = GCP.Firestore.Database("Links", {
type: "FIRESTORE_NATIVE",
});

Each Stack gets its own named database in Native mode, so it never touches the project’s (default) database.

Ask for the access you need: ReadDatabase, WriteDatabase, or ReadWriteDatabase:

src/Api.ts
import * as GCP from "@alchemy.run/gcp";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import { Links } from "./resources.ts";
export default class Api extends GCP.Function<Api>()(
"Api",
{ main: import.meta.url, invokerIamDisabled: true },
Effect.gen(function* () {
const db = yield* GCP.Firestore.ReadWriteDatabase(Links);
return {
fetch: Effect.gen(function* () {
const doc = yield* db.get("links/home");
return yield* HttpServerResponse.json(doc?.fields ?? {});
}).pipe(Effect.orDie),
};
}).pipe(Effect.provide(GCP.Firestore.ReadWriteDatabaseHttp)),
) {}

ReadWriteDatabase grants roles/datastore.user, limited to this one database. The service can’t touch any other database in the project.

get takes a path and returns undefined when the document is missing:

const doc = yield* db.get("links/aB3xY7z");
const url = doc?.fields.url;

Fields come back as JavaScript values: integers as number, timestamps as Date.

set creates or replaces a document:

yield* db.set("links/aB3xY7z", {
url: "https://alchemy.run",
clicks: 0,
createdAt: new Date(),
});

update changes only the keys you pass and leaves the rest alone:

yield* db.update("links/aB3xY7z", { clicks: 1 });

create fails with DocumentAlreadyExists instead of overwriting:

const created = yield* db.create(`links/${code}`, { url }).pipe(
Effect.as(true),
Effect.catchTag("GCP.Firestore.DocumentAlreadyExists", () =>
Effect.succeed(false),
),
);
const { documents, nextPageToken } = yield* db.list("links", {
pageSize: 50,
});

query runs a Firestore structured query. Filter values use Firestore’s value shape:

const admins = yield* db.query({
from: [{ collectionId: "users" }],
where: {
fieldFilter: {
field: { fieldPath: "role" },
op: "EQUAL",
value: { stringValue: "admin" },
},
},
});
yield* db.delete("links/aB3xY7z");

Deleting a missing document succeeds.

The grant is a conditional project IAM binding, and those take a few minutes to reach Firestore. A brand-new service can see Forbidden until then. Retry the first call in tests.

Run code whenever a document is created with Eventarc.

examples/gcp-cloud-run-api is a link shortener over Firestore, with its API key in Secret Manager.