Firebase logo

Firebase

Community
ericrisco
firebase

Use when building on Firebase — Firestore data modeling, Security Rules, Auth and custom claims, Cloud Functions, Storage, modular Web/Admin SDK imports — including symptoms like a database open to the internet, a query rejected by rules, or a doc stuck at ~1 write/sec. NOT managed-Postgres BaaS with SQL and RLS (that is supabase).

Overview

Publisherericrisco
Repositoryrsc-harness
Skill namefirebase
Stars
239
Forks
24
Bundled files
5
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 5 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by ericrisco on GitHub. Read the source before you install it.

Installation

Install the Firebase AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/ericrisco/rsc-harness/tree/main/skills/firebase
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/ericrisco/rsc-harness.git /tmp/rsc-harness
mkdir -p .claude/skills
cp -r /tmp/rsc-harness/skills/firebase .claude/skills/firebase
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Firebase in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Firebase on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Firebase is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Firebase — Firestore, Rules, Auth, Functions, Storage

The Firebase product surface that sits on top of GCP, on the modular Web SDK (v12) and the Admin SDK. The whole skill exists to stop two failure modes: dragging relational/SQL habits into a NoSQL document store, and leaving the database open to the internet.

Two facts drive everything below:

  • It is a NoSQL document store, shaped for the read path. No joins, no server-side OR across different fields without a composite index, no SELECT * across collections. Denormalize and fan-out so a screen is one cheap query — reads are what you pay for and what users wait on.
  • Rules ARE the access control. Firestore is reachable directly from untrusted clients. There is no app server in the trust path by default — firestore.rules (CEL) is the only thing between a browser and your data. App Check attests the request even came from your app before Rules evaluate.

Not this skill:

Instead of FirebaseGo to
Relational schema, SQL, EXPLAIN, indexing a SQL engine../postgresdb/SKILL.md
Managed Postgres BaaS (SQL + Postgres RLS + PostgREST) — the most-confused sibling: same "backend-as-a-service" shape, completely different data model and rules languagesupabase
AWS document/key-value store with its own capacity modeldynamodb
Self-hosted Mongo document modelingmongodb
Generic GCP project/IAM/billing not specific to a Firebase productgcp-essentials
React/Next.js component or rendering work that merely calls Firebasereact / ../nextjs/SKILL.md

Data modeling

Firestore charges and waits on reads. Model so the common screen is one query against one collection.

Collection vs subcollection vs root + denormalized field — decide by access pattern:

ShapeUse whenWhy
Subcollection (rooms/{id}/messages)Child list is only ever read inside its parent, can grow unboundedSubcollections don't bloat the parent doc; deleting a parent does NOT delete them (handle that)
Separate root collection + foreign idChild must be queried across all parents (collection-group query)A collectionGroup('messages') query needs the docs in same-named subcollections OR a root collection
Denormalized field on the parentA few values are shown alongside the parent and rarely changeAvoids a second read; you accept writing the copy on every change

Hard limits — design around them, don't discover them in prod:

  • A document maxes out at 1 MiB (1,048,576 bytes). Don't accumulate an unbounded array (chat messages, audit log) inside one doc — it will hit the wall and every read pays for the whole blob. Use a subcollection.
  • A single document tolerates only ~1 sustained write/sec. Monotonic IDs and indexed sequential timestamps create a hotspot on one index range. Use scattered auto-IDs (doc(collection(db,'x'))), and for high-frequency counters use a sharded counter (N shard docs, sum on read).

Query reality: no joins; range/inequality filters on a field plus an orderBy on another field require a composite index; in / array-contains-any are capped (~30 values). If a query needs an index, declare it in firestore.indexes.json — see the emulator gotcha below.

ts
// Bad — unbounded array inside one doc; hits 1 MiB, every read pays for all of it
await setDoc(doc(db, "rooms", roomId), { messages: [...allMessages, newMsg] });

// Good — one doc per message in a subcollection, scattered auto-ID, no hotspot
await addDoc(collection(db, "rooms", roomId, "messages"), {
  text, authorId, createdAt: serverTimestamp(),
});

Denormalization recipes, counter sharding, cursor pagination, getCountFromServer, collection-group queries, and composite-index design live in references/data-modeling.md.

Security Rules — the load-bearing section

Rules are NOT filters. A query is rejected outright unless the rules can guarantee every matched document is readable — Firestore will not silently drop the docs you can't see. So a list rule and the query that runs against it must agree: if the rule allows reading only your own docs, the query must itself be constrained (where("ownerId","==",uid)), or the whole query fails.

javascript
// Bad — the entire database is readable AND writable by anyone on the internet
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} { allow read, write: if true; }
  }
}

// Good — default-deny, ownership-scoped, with create-time validation
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /posts/{postId} {
      allow get:    if resource.data.ownerId == request.auth.uid;
      allow list:   if request.auth != null;            // query MUST add where(ownerId == uid)
      allow create: if request.auth.uid == request.resource.data.ownerId;
      allow update, delete: if resource.data.ownerId == request.auth.uid;
    }
    // everything else: no rule = denied
  }
}

Rules to internalize:

  • Default-deny. No matching allow = denied. Never add a /{document=**} catch-all with if true. That single line is the "open to the internet" headline risk.
  • request.auth is the authenticated identity (null when signed out); request.auth.token carries custom claims for RBAC (e.g. request.auth.token.admin == true).
  • resource.data is the existing doc; request.resource.data is the incoming write. Validate the incoming write on create/update (types, immutable ownerId, no privilege escalation).
  • get() / exists() read another doc for cross-document checks (e.g. role lookup) — each costs a billed read and counts against rule-evaluation limits, so keep them shallow.
  • get vs list are distinct: a single-doc read vs a query. read = both; split them so a query can't leak documents a single get would also have blocked.

Set custom claims with the Admin SDK, never from the client. Add App Check in production so Rules only run for requests that provably came from your real app.

Full CEL patterns — RBAC via claims, ownership, validation functions, time-based throttling, and the complete @firebase/rules-unit-testing recipe — are in references/security-rules.md.

Auth

  • Client sign-in with getAuth() + a provider; the SDK manages the refresh of the ID token.
  • Server-side, verify the ID token with getAuth(adminApp).verifyIdToken(idToken) before trusting any caller. A raw UID from the client is not proof of anything.
  • Custom claims for RBAC: getAuth(adminApp).setCustomUserClaims(uid, { admin: true }). Claims land in request.auth.token in Rules and in the decoded token on the server. They refresh on the client's next token refresh, not instantly — force a refresh if you need it immediately.
  • Session cookies (createSessionCookie) suit SSR / server-rendered apps where you want an httpOnly cookie instead of shipping the ID token to every request — pairs with ../nextjs/SKILL.md.

Cloud Functions (2nd gen)

2nd gen is the default and the only generation that runs Node.js 22. Use firebase-functions v7 modular triggers and firebase-admin.

ts
import { onDocumentWritten } from "firebase-functions/v2/firestore";
import { onCall, HttpsError } from "firebase-functions/v2/https";
import { defineSecret } from "firebase-functions/params";

const STRIPE_KEY = defineSecret("STRIPE_KEY"); // never hard-code secrets

export const onPostWrite = onDocumentWritten(
  { document: "posts/{postId}", region: "europe-west1" },
  async (event) => {
    // Background events deliver AT-LEAST-ONCE — make this idempotent.
    const eventId = event.id; // dedupe on this (e.g. a processed/{eventId} marker doc)
  }
);

export const setAdminClaim = onCall(async (request) => {
  if (request.auth?.token.admin !== true) throw new HttpsError("permission-denied", "admins only");
  // ... verify, then setCustomUserClaims via admin SDK
});
  • Callable (onCall) gives you request.auth already verified; raw onRequest HTTPS does not — you must verify the ID token yourself.
  • Idempotency is mandatory for background triggers (onDocumentWritten etc.): events can fire more than once, so guard side effects with the event id.
  • Pin region, set secrets with defineSecret (not env literals), and tune concurrency for cost.
  • Functions require the Blaze plan; outbound networking from a function also requires Blaze.

Trigger catalogue, callable-vs-HTTPS auth, idempotency keys, cold-start/cost, Auth blocking functions, and region pinning are in references/cloud-functions.md.

Cloud Storage

Storage paths are gated by their own Rules; clients can hit them directly.

javascript
// Bad — any signed-in user can overwrite any other user's avatar
match /avatars/{fileName} { allow write: if request.auth != null; }

// Good — path-scoped to the owner, with a size/type guard
match /avatars/{uid}/{fileName} {
  allow read:  if true;                          // public avatars
  allow write: if request.auth.uid == uid
               && request.resource.size < 5 * 1024 * 1024
               && request.resource.contentType.matches('image/.*');
}

For server-issued time-limited access (private downloads), generate a signed URL from the Admin SDK rather than loosening the Rules.

SDK & project mechanics

Use the modular SDK so the bundler tree-shakes unused Firebase code. The old namespaced firebase.firestore() API is gone in v9+.

ts
// Bad — pulls the entire SDK; defeats tree-shaking (and the compat/namespaced API is legacy)
import firebase from "firebase";
firebase.firestore().collection("posts").get();

// Good — named imports, only what you use ships (Web SDK v12)
import { initializeApp } from "firebase/app";
import { getFirestore, collection, getDocs } from "firebase/firestore";
const db = getFirestore(initializeApp(config));
const snap = await getDocs(collection(db, "posts"));
  • firebase.json configures emulators, rules/index file paths, and hosting; firestore.indexes.json declares composite indexes.
  • Run the Local Emulator Suite (firebase emulators:start) for local dev and tests.
  • Emulator gotcha: the Firestore emulator does NOT enforce composite indexes — it runs any valid query. So "works in the emulator, fails in prod with requires an index" is expected. Verify index coverage separately by keeping firestore.indexes.json in sync and deploying it.
  • The Firebase API key in client config is not a secret (it identifies the project, not authorizes access — Rules + App Check do that). Service-account JSON keys ARE secrets; keep them server-side.

Anti-patterns

Anti-patternWhy it's wrongDo instead
allow read, write: if true; catch-allWhole DB is open to the internetDefault-deny; scope each match to request.auth + ownership
Treating rules as query filtersQuery is rejected, not filtered — it fails entirelyConstrain the query to match what list allows
Unbounded array in one documentHits the 1 MiB limit; every read pays for the whole blobSubcollection, one doc per item
Monotonic IDs / sequential indexed timestampsIndex hotspot → ~1 write/sec/doc wallScattered auto-IDs; sharded counters for high write rate
Trusting client writes for sensitive fieldsClient can set role: "admin" on itselfValidate request.resource in Rules; set claims via Admin SDK only
No App Check in productionRules run for any caller, including scripts/scrapersEnable App Check (reCAPTCHA / Play Integrity / App Attest)
Service-account key in client / repoFull admin access leaksKeep service-account JSON server-side; client API key is fine
Namespaced/compat SDK (firebase.firestore())Legacy, not tree-shakeable, gone in modularModular named imports from firebase/firestore
No emulator / rules testsOpen or broken rules ship silently@firebase/rules-unit-testing via firebase emulators:exec
Background trigger with non-idempotent side effectsAt-least-once delivery double-charges/double-writesDedupe on event.id

Verify

scripts/verify.sh is read-only and runs from your project root. It locates firestore.rules and fails if a root match /{document=**} carries an allow read, write: if true; catch-all or the rules file is empty; validates firestore.indexes.json parses as JSON; and, when the Firebase CLI is present, points at the firebase emulators:exec rules-test path. It exits 0 and skips cleanly when no Firebase artifacts are in the working directory — not every repo has them.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Firebase AI skill do?

Use when building on Firebase — Firestore data modeling, Security Rules, Auth and custom claims, Cloud Functions, Storage, modular Web/Admin SDK imports — including symptoms like a database open to the internet, a query rejected by rules, or a doc stuck at ~1 write/sec. NOT managed-Postgres BaaS with SQL and RLS (that is supabase).

Why use Firebase on TypingMind?

Because you install it once and use it with any model. Firebase is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Firebase in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/ericrisco/rsc-harness/tree/main/skills/firebase. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Firebase?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Firebase?

As many as you like. As long as a model supports skills, you can use Firebase with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Firebase AI skill free?

Yes. It is published on GitHub by ericrisco under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇