Antigravity × Firebase Data Connect: Building Type-Safe GraphQL APIs Faster with AI
A complete implementation guide for Firebase Data Connect. From GraphQL schema design to Firebase Auth integration, Antigravity AI-assisted query generation, and production deployment—all with working code examples.
Firebase Data Connect has been generally available for over a year now, yet plenty of developers are still in the "I'll try it someday" camp. A GraphQL-based relational database connector that sits between Cloud SQL and the Firebase ecosystem—what does it actually add to a project that already runs on Firestore?
I had the same question. Then I opened a schema file inside Antigravity IDE and the AI immediately flagged: "With this entity structure, adding @table directives and restructuring like this will eliminate all N+1 queries." That moment made it clear Firebase Data Connect reshapes Firebase development at a fundamental level.
This guide shares what I learned building with Firebase Data Connect and Antigravity together—with working code and an honest look at the pain points that official docs tend to gloss over.
Firebase Data Connect vs Firestore: Understanding the Real Difference
Firebase Data Connect connects Google Cloud SQL (PostgreSQL) to the Firebase ecosystem through a GraphQL interface. It reached GA in 2024 and represents a fundamentally different design philosophy from Firestore and Realtime Database.
The most important shift is schema-first development. Firestore's schemaless flexibility was a selling point, but Data Connect requires you to define your schema in .gql files first—then client SDKs, TypeScript types, and GraphQL resolvers are auto-generated from those definitions.
When Data Connect fits better than Firestore:
Complex relational data: users, posts, comments, likes with foreign keys
Aggregate queries (SUM, AVG, GROUP BY) for analytics features
Migrating an existing PostgreSQL schema into the Firebase ecosystem
Teams that want type-safe GraphQL without building a custom API layer
When Firestore remains the better choice:
Heavy use of real-time listeners (onSnapshot) for live updates
Frequently changing document structures that don't fit a rigid schema
Small-scale start where you want to delay schema decisions
When you ask Antigravity to evaluate your domain model, it analyzes your entities and explains which ones suit Data Connect versus which should stay in Firestore—and why.
Setting Up Your Development Environment the Right Way
Firebase Data Connect setup has a few non-obvious traps. Get the sequence wrong and you'll spend hours troubleshooting issues that stem from initialization order.
Start by ensuring your Firebase CLI is recent enough:
# Update Firebase CLI (version 12.0.0+ required)npm install -g firebase-tools@latestfirebase --version # Confirm 13.x.x# Initialize a projectmkdir my-app && cd my-appfirebase init dataconnect# Set up emulators (run Data Connect + Auth together)firebase init emulators# Select: dataconnect, auth, hosting
When you open the project in Antigravity, it will suggest adding a Data Connect context to .antigravity/rules. Accept this: it enables accurate GraphQL directive autocomplete in schema files.
# Suggested addition to .antigravity/rules
You are working on a Firebase Data Connect project.
Schema files use .gql extension with @table, @col, @ref directives.
Connector files define queries and mutations.
Always generate TypeScript SDK imports from the auto-generated client SDK.
The project structure that Data Connect expects looks like this:
One common early mistake: merging schema.gql and queries.gql into a single file. Data Connect's build process assumes they're separate. Mixing them causes the emulator to fail in ways that produce confusing error messages.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Developers confused about when to use Cloud SQL vs Firestore will gain clarity and be able to design and implement type-safe GraphQL APIs from scratch
✦You'll master specific prompt patterns for using Antigravity's AI to generate schemas and optimize queries, boosting development speed 3–5x
✦By the end, you'll have implemented Firebase Auth integration, Row Level Security, and production performance tuning—ready to ship
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Schema Design: Mastering the Core GraphQL Directives
Three directives form the backbone of Data Connect schema design: @table, @col, and @ref. When you ask Antigravity to generate a blog service schema, it produces something like this:
@default(expr: "uuidV4()"): Data Connect generates UUIDs server-side. Clients never send an ID—they receive one. This eliminates UUID collision risks entirely.
@manyToMany(joinTable: "PostTag"): No need to explicitly define the join table. Data Connect creates PostTag automatically. The catch: if you need additional attributes on the join table (like a sort order), you'll need an explicit entity definition. Antigravity handles this case well if you describe what you need.
@col(dataType: "varchar(255)"): You're specifying the underlying PostgreSQL type directly. The default for String is text, which is fine for most cases but varchar is more efficient for indexed columns with predictable length bounds.
Queries and Mutations: Where Type Safety Pays Off
Once the schema is in place, you define queries and mutations in separate connector files. These definitions compile directly into a typed TypeScript SDK—which is the feature that makes Data Connect meaningfully different from writing your own API.
# dataconnect/connector/queries.gql# Public posts list with paginationquery ListPublishedPosts($limit: Int, $offset: Int) @auth(level: PUBLIC) { posts( where: { published: { eq: true } } orderBy: [{ createdAt: DESC }] limit: $limit offset: $offset ) { id title createdAt author { id displayName } tags { name } }}# Current user's posts (authentication required)query GetUserPosts($userId: UUID\!) @auth(level: USER) { posts( where: { authorId: { eq: $userId } _and: [{ authorId: { eq_expr: "auth.uid" } }] } orderBy: [{ createdAt: DESC }] ) { id title published createdAt updatedAt }}
When you write post.title, TypeScript knows it's a string. That guarantee simply doesn't exist with Firestore's DocumentData.
Firebase Auth Integration: The @auth Directive Patterns That Actually Matter
Data Connect access control is defined through @auth directives on each query and mutation. Getting this wrong is the most common source of security vulnerabilities in Data Connect apps.
The four auth levels:
@auth(level: PUBLIC): Anyone can execute, including unauthenticated users. Use for reading public content.
@auth(level: USER): Requires a valid Firebase Auth token. The auth.uid expression becomes available.
@auth(level: ADMIN): Only Firebase Admin SDK can execute. Useful for batch operations and server-side jobs.
@auth(level: USER_ANON): Includes anonymous auth users along with authenticated ones.
Here's the security trap that Antigravity will warn you about. @auth(level: USER) alone does NOT prevent authenticated users from modifying other users' data:
# ❌ Dangerous: any logged-in user can delete anyone's postmutation DeletePost($postId: UUID\!) @auth(level: USER) { post_delete(id: $postId)}# ✅ Safe: users can only delete their own postsmutation DeletePost($postId: UUID\!) @auth(level: USER) { post_delete( id: $postId where: { authorId: { eq_expr: "auth.uid" } } )}
Client-side Auth integration:
// src/lib/auth-dataconnect.tsimport { getAuth, onAuthStateChanged } from "firebase/auth";import { getDataConnect, setInitialUserToken } from "firebase/data-connect";import { connectorConfig } from "@firebasegen/my-app-connector";const auth = getAuth();const dc = getDataConnect(connectorConfig);onAuthStateChanged(auth, async (user) => { if (user) { const token = await user.getIdToken(); await setInitialUserToken(dc, token); } else { await setInitialUserToken(dc, undefined); }});
Note that setInitialUserToken is asynchronous. Running a Data Connect query immediately after calling it—before the token is applied—will result in permission errors. The pattern that works: place your post-login queries inside the onAuthStateChanged callback.
Using Antigravity AI for Schema Optimization: Prompt Patterns That Work
The highest-leverage use of Antigravity with Data Connect is as a schema review and optimization partner. Here are the prompts that consistently produce useful output:
Pattern 1: N+1 query detection
Analyze the current schema.gql and queries.gql. Identify every query
where N+1 queries could occur. For each one, explain why it happens
and rewrite it using nested selection or @join to resolve it in a single query.
Antigravity will typically respond with something like: "The ListPublishedPosts query fetches author in a separate query for each post. Data Connect automatically converts nested selections into JOINs, so restructuring like this reduces it to one round trip..."
Pattern 2: Index planning
Based on the WHERE and ORDER BY clauses in queries.gql, identify which
indexes should be added to Cloud SQL. Generate the CREATE INDEX SQL
statements I should add to my migration file.
Data Connect manages basic indexes automatically, but composite and partial indexes often need manual addition. The AI analyzes your query patterns and produces actionable SQL.
Pattern 3: Pagination strategy
I'm currently using offset-based pagination. Explain when I should
migrate to cursor-based pagination and show me a Data Connect implementation
using timestamp-based cursors as a practical alternative.
Five Real Pitfalls (With Solutions)
These are the issues that actually cost me time when building with Data Connect:
1. Emulator vs. production schema drift
The emulator ignores schemaValidation: COMPATIBLE in dataconnect.yaml and always uses strict validation. If you see schema errors only in production, the usual cause is @col(dataType) missing from a field—Cloud SQL creates it as text while the query expects varchar.
Solution: Run firebase dataconnect:sql:migrate --dry-run before every production deploy to preview the exact SQL that will execute.
2. Cache staleness with Next.js App Router
After a successful mutation, calling router.refresh() sometimes still shows stale data. This isn't a Data Connect SDK issue—it's Next.js RSC cache. Data Connect's client SDK doesn't cache responses, so the staleness comes from Next.js's server-side cache layer.
Solution: Call revalidatePath() inside a Server Action after every mutation:
// src/app/actions/posts.ts"use server";import { revalidatePath } from "next/cache";import { executeMutation, createPostRef } from "@firebasegen/my-app-connector";import { dc } from "@/lib/dataconnect";export async function createPost(title: string, content: string) { try { const result = await executeMutation( createPostRef(dc, { title, content }) ); revalidatePath("/posts"); return { success: true, post: result.data.post_insert }; } catch (error) { console.error("Create post error:", error); return { success: false, error: "Failed to create post" }; }}
3. UUID format validation failures
The TypeScript SDK types UUIDs as string, but Data Connect validates UUID format server-side. Passing a non-UUID string causes a runtime error with a message that doesn't always make the source clear.
Solution: Validate inputs before they reach the SDK:
import { z } from "zod";const uuidSchema = z.string().uuid("Invalid ID format");export async function getPost(postId: string) { const validatedId = uuidSchema.parse(postId); try { const result = await executeQuery( getPostDetailRef(dc, { postId: validatedId }) ); return result.data.post; } catch (error) { if (error instanceof z.ZodError) { throw new Error("Invalid request"); } throw error; }}
4. Timezone issues with request.time
@default(expr: "request.time") stores timestamps in UTC. Japanese users will see times offset by 9 hours if you display raw timestamps without conversion.
offset: 50000 on a 100k-row table forces PostgreSQL to skip through 50,000 rows before returning results. This gets slow fast. Data Connect's cursor-based pagination is experimental as of April 2026, so the practical workaround is filtering by timestamp:
# Use last item's createdAt as a cursor instead of offsetquery ListPostsBefore($before: Date\!, $limit: Int) @auth(level: PUBLIC) { posts( where: { published: { eq: true } createdAt: { lt: $before } } orderBy: [{ createdAt: DESC }] limit: $limit ) { id title createdAt }}
Production Deployment and Operational Best Practices
For your first production deploy, the most consequential decision is Cloud SQL instance tier. Development edition starts around $7/month, while Enterprise starts around $30/month with significantly better performance guarantees and SLAs.
For solo projects and early-stage startups, Development edition is the right starting point. Evaluate migration to Enterprise when you're consistently seeing latency spikes or when DAU crosses ~1,000.
# Deploy to productionfirebase deploy --only dataconnect# Run schema migration (for breaking changes)firebase dataconnect:sql:migrate --force# Recommended: dry run before any migrationfirebase dataconnect:sql:migrate --dry-run
Connection pooling deserves special attention when using Data Connect with serverless runtimes (Cloudflare Workers, Vercel Edge Functions). Each serverless invocation can create a new connection to Cloud SQL, and Cloud SQL's default connection limit is 100 for Development tier. The Data Connect SDK manages connection pools internally, but in high-concurrency environments you may need explicit configuration via @google-cloud/cloud-sql-connector.
Connecting to MCP: Letting AI Agents Query Your Database
One of the more powerful combinations is exposing Data Connect through an MCP server so Antigravity's AI can query your actual data during development. The Antigravity × MCP Ecosystem 2026 Guide covers the MCP architecture in depth, but here's a minimal Data Connect tool registration:
// mcp-server/src/tools/dataconnect.tsimport { Server } from "@modelcontextprotocol/sdk/server/index.js";import { executeQuery, listPublishedPostsRef } from "@firebasegen/my-app-connector";import { dc } from "../lib/dataconnect.js";export function registerDataConnectTools(server: Server) { server.setRequestHandler("tools/call", async (request) => { if (request.params.name === "get_recent_posts") { const { limit = 5 } = request.params.arguments as { limit?: number }; try { const result = await executeQuery( listPublishedPostsRef(dc, { limit }) ); return { content: [{ type: "text", text: JSON.stringify(result.data.posts, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }], isError: true }; } } });}
With this MCP server connected to Antigravity, the AI can fetch real post data in response to natural language requests during development sessions—useful for generating realistic test scenarios or analyzing production data patterns without leaving the IDE.
For multi-agent patterns that extend this further, see Antigravity × Agent Manager Framework.
What Changes When You Adopt Data Connect
Firebase Data Connect is not a Firestore replacement—it's the right tool when your domain has relational structure that doesn't fit comfortably into document collections. The GraphQL schema-first approach generates a typed client SDK that eliminates an entire category of runtime type errors that Firestore developers routinely encounter.
Paired with Antigravity, the development experience improves further: N+1 detection, index suggestions, and security rule reviews happen in the editor instead of in post-incident retrospectives.
The next concrete step: run firebase init dataconnect, open the generated schema file in Antigravity, and define your first entity. The moment a typed query compiles and returns structured data, the value of the schema-first approach becomes immediately tangible.
Performance Tuning: Getting Fast Queries at Scale
Beyond the initial setup, long-term performance requires deliberate tuning. Here are the patterns that matter most in production Data Connect deployments.
Query Complexity Budget
Data Connect automatically resolves nested relations, which is convenient but can hide expensive JOIN chains. A query that fetches posts with authors, tags, and the author's recent comment history may look like a single operation but translates to multiple JOIN operations in PostgreSQL.
The practical rule: limit nested relation depth to two levels in queries serving high-traffic endpoints. For deeper data needs, consider splitting into multiple targeted queries and composing results client-side.
# High-traffic endpoint: keep relations shallowquery ListPostsForFeed($limit: Int) @auth(level: PUBLIC) { posts( where: { published: { eq: true } } orderBy: [{ createdAt: DESC }] limit: $limit ) { id title createdAt # One level of relation is fine author { id displayName } # Tags are a flat array—safe to include tags { name } # Avoid: author's recent posts, author's followers, etc. }}
Read Replicas for Analytics Queries
Cloud SQL Enterprise tier supports read replicas. For analytics-heavy queries (aggregations, full-table scans), routing to a read replica prevents those queries from competing with write operations on the primary instance.
Data Connect doesn't natively expose replica routing, but you can achieve this by maintaining two Data Connect connectors—one configured for the primary instance and one for the replica—and selecting the appropriate connector in your application based on query intent.
// src/lib/dataconnect-analytics.tsimport { getDataConnect } from "firebase/data-connect";import { replicaConnectorConfig } from "@firebasegen/my-app-connector-analytics";// Separate connector pointing at read replicaexport const dcAnalytics = getDataConnect(replicaConnectorConfig);
Batching Inserts for Write-Heavy Workloads
When importing data or processing events in bulk, individual post_insert calls add up quickly in both latency and Cloud SQL cost. Data Connect provides post_insertMany for batch operations:
// src/app/actions/batch-import.ts"use server";import { executeMutation, postInsertManyRef } from "@firebasegen/my-app-connector";import { dc } from "@/lib/dataconnect";export async function importPosts( posts: Array<{ title: string; content: string }>) { // Validate inputs first if (posts.length === 0) return { imported: 0 }; if (posts.length > 500) { throw new Error("Batch size exceeds limit of 500"); } try { const result = await executeMutation( postInsertManyRef(dc, { data: posts.map((p) => ({ title: p.title, content: p.content, // authorId_expr resolves at server time authorId_expr: "auth.uid", })), }) ); return { imported: result.data.post_insertMany.length }; } catch (error) { console.error("Batch import failed:", error); throw new Error("Import failed. Check server logs for details."); }}
A single post_insertMany call with 100 records costs roughly the same as 3–5 individual inserts in Cloud SQL compute terms, while producing the same 100 rows. For any import workflow, batching is non-negotiable.
Testing Data Connect Applications with Antigravity
Testing Data Connect applications requires a slightly different approach from testing Firestore apps, because you're working with a typed SDK generated from schema definitions. Antigravity helps here by generating test scaffolding that matches your connector types.
Unit Testing Query Logic
For testing the application layer that calls Data Connect, mock the generated SDK functions rather than the network layer:
For integration tests that exercise the full stack including actual Data Connect queries, configure your test runner to start the Firebase emulator suite:
// vitest.setup.tsimport { initializeApp } from "firebase/app";import { connectDataConnectEmulator, getDataConnect,} from "firebase/data-connect";import { connectorConfig } from "@firebasegen/my-app-connector";const app = initializeApp({ projectId: "demo-test-project", // Other config as needed});const dc = getDataConnect(app, connectorConfig);connectDataConnectEmulator(dc, "localhost", 9399);// Export for use in testsexport { dc };
The emulator supports resetting state between tests via the REST API, which makes it practical for integration test suites that need a clean slate for each test case.
Asking Antigravity to "generate test cases for the CreatePost mutation covering auth success, auth failure, and validation edge cases" produces a complete test file tailored to your specific connector definitions. This is considerably faster than writing tests from scratch and avoids the common mistake of testing only the happy path.
Monitoring and Observability in Production
Once your Data Connect-backed application is in production, you need visibility into query performance, error rates, and connection pool behavior.
Cloud SQL Insights
Cloud SQL Insights (available on Enterprise tier) provides query-level performance data: execution time, wait time, lock contention, and execution plans. For slow queries identified here, Antigravity can analyze the query definition and suggest index additions or structural changes.
Enable it in the Cloud Console under your Cloud SQL instance settings. No code changes are required—it instruments at the database layer.
Custom Logging for Data Connect Operations
Data Connect SDK errors don't always surface with enough context for debugging. Adding a thin wrapper that logs query names and timing produces actionable data:
// src/lib/dataconnect-instrumented.tsimport { executeQuery as sdkExecuteQuery, executeMutation as sdkExecuteMutation } from "firebase/data-connect";type QueryRef = Parameters<typeof sdkExecuteQuery>[0];type MutationRef = Parameters<typeof sdkExecuteMutation>[0];export async function executeQuery<T>(ref: QueryRef) { const start = Date.now(); const name = (ref as any).name ?? "unknown-query"; try { const result = await sdkExecuteQuery<T>(ref as any); const duration = Date.now() - start; if (duration > 1000) { console.warn(`[DataConnect] Slow query "${name}": ${duration}ms`); } return result; } catch (error) { const duration = Date.now() - start; console.error(`[DataConnect] Query "${name}" failed after ${duration}ms:`, error); throw error; }}export async function executeMutation<T>(ref: MutationRef) { const start = Date.now(); const name = (ref as any).name ?? "unknown-mutation"; try { const result = await sdkExecuteMutation<T>(ref as any); const duration = Date.now() - start; if (duration > 2000) { console.warn(`[DataConnect] Slow mutation "${name}": ${duration}ms`); } return result; } catch (error) { console.error(`[DataConnect] Mutation "${name}" failed:`, error); throw error; }}
Replace SDK imports with this instrumented version throughout your application. The overhead is negligible (a single Date.now() call) and the observability payoff is significant when debugging production issues.
When to Reach for AgentKit with Data Connect
For applications that go beyond read/write CRUD patterns—where AI agents need to reason about data and take multi-step actions—combining Data Connect with AgentKit 2.0's production patterns creates a powerful foundation. An agent might query Data Connect to assess current state, reason about the results, and then write back conclusions—all within a single coordinated workflow rather than requiring manual orchestration.
The type safety of Data Connect's generated SDK is particularly valuable in agent contexts: when an agent receives tool call results, strongly typed return values reduce the chance of hallucinations about data structure that can occur when working with untyped JSON.
Final Thoughts
Firebase Data Connect fills a genuine gap in the Firebase ecosystem. Firestore remains the right choice for real-time sync and flexible document structures. But for applications with relational data, aggregate queries, and teams that want type safety from database schema to UI component, Data Connect removes the need for a custom GraphQL API layer entirely.
The combination with Antigravity is particularly effective because schema design, query optimization, and security review are all tasks where AI assistance meaningfully reduces the time to get things right. You'll still need to understand what you're building—the pitfalls described here are real and the AI won't catch all of them—but the feedback loop is tight enough that most issues surface during development rather than in production.
Run firebase init dataconnect. Write your first schema. Let Antigravity suggest the indexes. The type-safe query that comes back will feel qualitatively different from what you've been working with in Firestore.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.