All Articles
CategoryBackend
Reading Time
23 min read
Published
2026-02-13
Word Count
2,528words

Grab a coffee — this one is a deep dive!

Firebase Data Connect: GraphQL + Cloud SQL

Summary

Type-safe SDK, schema-first approach, PostgreSQL backend, auto-generated queries, real-time sync, and an in-depth integration guide with the Firebase ecosystem.

  • Firebase Data Connect is a schema-first GraphQL API service that runs on Cloud SQL for PostgreSQL.
  • Type-safe SDKs for iOS, Android, Flutter, and Web are auto-generated from the GraphQL schema.
  • PostgreSQL tables and relationships are defined using the @table, @unique, @relation, and @index directives.
  • Auth levels are offered in five tiers: PUBLIC, USER_ANON, USER, USER_EMAIL_VERIFIED, and NO_ACCESS.
Firebase Data Connect: GraphQL + Cloud SQL

Firebase's biggest criticism has always been the same: "Firestore is powerful, but its relational data modeling is weak." Google solved this problem with Firebase Data Connect. A service built on Cloud SQL (PostgreSQL), queried with GraphQL, that automatically generates type-safe SDKs. Now you can combine Firebase's ease of use with PostgreSQL's power. Let's take a deep dive into Data Connect's architecture, its schema-first approach, auto-generated SDKs, and how to use it in production.

💡 Note: Firebase Data Connect reached GA in 2024. The information in this article is current as of February 2026. Official source: Data Connect Docs, backend: Cloud SQL, schema language: GraphQL Spec. We covered GraphQL fundamentals in our GraphQL Mobile article — here we focus on Firebase-specific details.

Table of Contents


What Is Data Connect?

Firebase Data Connect is a GraphQL API service that runs within the Firebase ecosystem, backed by Cloud SQL for PostgreSQL. You define the schema and queries, and Firebase automatically generates type-safe SDKs for iOS, Android, Flutter, and Web.

Architecture Overview

text
1Mobile/Web App
2 ↓ (Type-safe SDK)
3Firebase Data Connect Service
4 ↓ (GraphQL)
5Cloud SQL for PostgreSQL

Why Data Connect?

Problem
Firestore Solution
Data Connect Solution
Relational data
Subcollection + denormalization
Foreign key + JOIN
Complex query
Composite index + client filtering
SQL query power
Type safety
Weak (any-like)
Fully type-safe SDK
Schema
Schema-less (flexible but risky)
Schema-first (safe)
Transaction
Batch write (limited)
ACID transaction
Aggregate
Client-side computation
SUM, AVG, COUNT server-side
Migration
Manual
SQL migration

Setup

bash
1# Update Firebase CLI
2npm install -g firebase-tools@latest
3 
4# Initialize Data Connect
5firebase init dataconnect
6 
7# Project structure is created:
8# dataconnect/
9# ├-- schema/ → GraphQL schema files
10# ├-- connector/ → Query and mutation definitions
11# └-- dataconnect.yaml
🔍 Pro Tip: Data Connect can be added to your existing Firebase project. You don't need to abandon Firestore entirely — you can use Firestore for real-time sync and Data Connect for relational data.

Schema-First Approach

In Data Connect, you first define the database schema using GraphQL SDL. Firebase automatically converts this into PostgreSQL tables.

GraphQL Schema + Query Quick Start

graphql
1# dataconnect/schema/schema.gql — minimal blog schema
2type Post @table {
3 id: UUID! @default(expr: "uuidV4()")
4 title: String!
5 slug: String! @unique
6 content: String!
7 status: PostStatus! @default(value: "DRAFT")
8 author: User! @relation
9 publishedAt: Timestamp
10 createdAt: Timestamp! @default(expr: "request.time")
11}
12 
13enum PostStatus { DRAFT PUBLISHED ARCHIVED }
14 
15# connector/queries.gql — list published posts
16query ListPosts($limit: Int! = 10) @auth(level: PUBLIC) {
17 posts(
18 where: { status: { eq: PUBLISHED } }
19 orderBy: [{ publishedAt: DESC }]
20 limit: $limit
21 ) {
22 id title slug publishedAt
23 author { displayName }
24 }
25}

Once the schema is defined, you run the firebase dataconnect:sdk:generate command, and type-safe SDKs for TypeScript, Swift, and Dart are automatically generated. Generated SDK files should not be edited by hand.


Schema Definition

graphql
1# dataconnect/schema/schema.gql
2 
3# User table
4type User @table {
5 id: UUID! @default(expr: "uuidV4()")
6 email: String! @unique
7 displayName: String!
8 photoUrl: String
9 bio: String
10 createdAt: Timestamp! @default(expr: "request.time")
11 updatedAt: Timestamp! @default(expr: "request.time")
12}
13 
14# Blog post
15type Post @table {
16 id: UUID! @default(expr: "uuidV4()")
17 title: String!
18 slug: String! @unique
19 content: String!
20 excerpt: String
21 status: PostStatus! @default(value: "DRAFT")
22 publishedAt: Timestamp
23 author: User! @relation
24 category: Category @relation
25 viewCount: Int! @default(value: 0)
26 createdAt: Timestamp! @default(expr: "request.time")
27}
28 
29# Category
30type Category @table {
31 id: UUID! @default(expr: "uuidV4()")
32 name: String! @unique
33 slug: String! @unique
34 description: String
35 posts: [Post!]! @relation # Reverse relation
36}
37 
38# Comment
39type Comment @table {
40 id: UUID! @default(expr: "uuidV4()")
41 content: String!
42 author: User! @relation
43 post: Post! @relation
44 parentComment: Comment @relation # Nested comments
45 createdAt: Timestamp! @default(expr: "request.time")
46}
47 
48# Like (many-to-many)
49type Like @table(
50 key: ["user", "post"] # Composite primary key
51) {
52 user: User! @relation
53 post: Post! @relation
54 createdAt: Timestamp! @default(expr: "request.time")
55}
56 
57# Enum
58enum PostStatus {
59 DRAFT
60 PUBLISHED
61 ARCHIVED
62}

Schema Directives

Directive
Description
Example
@table
Creates a PostgreSQL table
type User @table
@unique
Unique constraint
email: String! @unique
@default
Default value
@default(value: 0)
@relation
Foreign key relationship
author: User! @relation
@index
Database index
@index(fields: ["status"])
@check
Constraint check
@check(expr: "rating >= 1")

Defining Queries and Mutations

After the schema, you define query (read) and mutation (write) operations:

Query Definitions

graphql
1# dataconnect/connector/queries.gql
2 
3# Get all published posts (with pagination)
4query ListPublishedPosts(
5 $limit: Int! = 10,
6 $offset: Int! = 0,
7 $categorySlug: String
8) @auth(level: PUBLIC) {
9 posts(
10 where: {
11 status: { eq: PUBLISHED },
12 category: { slug: { eq: $categorySlug } }
13 },
14 orderBy: [{ publishedAt: DESC }],
15 limit: $limit,
16 offset: $offset
17 ) {
18 id
19 title
20 slug
21 excerpt
22 publishedAt
23 viewCount
24 author {
25 displayName
26 photoUrl
27 }
28 category {
29 name
30 slug
31 }
32 }
33}
34 
35# Single post detail (by slug)
36query GetPostBySlug($slug: String!) @auth(level: PUBLIC) {
37 post: posts_findMany(where: { slug: { eq: $slug }, status: { eq: PUBLISHED } }) {
38 id
39 title
40 slug
41 content
42 publishedAt
43 viewCount
44 author {
45 id
46 displayName
47 photoUrl
48 bio
49 }
50 category {
51 name
52 slug
53 }
54 comments(orderBy: [{ createdAt: DESC }]) {
55 id
56 content
57 createdAt
58 author {
59 displayName
60 photoUrl
61 }
62 }
63 }
64}
65 
66# User profile and stats
67query GetUserProfile($userId: UUID!) @auth(level: USER) {
68 user(id: $userId) {
69 id
70 displayName
71 email
72 bio
73 photoUrl
74 posts_on_author(where: { status: { eq: PUBLISHED } }) {
75 id
76 title
77 viewCount
78 }
79 comments_on_author {
80 id
81 }
82 likes_on_user {
83 post {
84 id
85 title
86 }
87 }
88 }
89}

Mutation Definitions

graphql
1# dataconnect/connector/mutations.gql
2 
3# Create a new post
4mutation CreatePost($data: Post_Data!) @auth(level: USER) {
5 post_insert(data: $data)
6}
7 
8# Update post (author only)
9mutation UpdatePost(
10 $postId: UUID!,
11 $title: String,
12 $content: String,
13 $status: PostStatus
14) @auth(expr: "auth.uid == this.author.id") {
15 post_update(
16 id: $postId,
17 data: {
18 title: $title,
19 content: $content,
20 status: $status,
21 updatedAt_expr: "request.time"
22 }
23 )
24}
25 
26# Add a comment
27mutation AddComment(
28 $postId: UUID!,
29 $content: String!,
30 $parentCommentId: UUID
31) @auth(level: USER) {
32 comment_insert(data: {
33 content: $content,
34 post: { id: $postId },
35 author: { id_expr: "auth.uid" },
36 parentComment: { id: $parentCommentId }
37 })
38}
39 
40# Toggle like
41mutation ToggleLike($postId: UUID!) @auth(level: USER) {
42 like_upsert(data: {
43 user: { id_expr: "auth.uid" },
44 post: { id: $postId }
45 })
46}
47 
48# Delete post (soft delete → ARCHIVED)
49mutation ArchivePost($postId: UUID!) @auth(expr: "auth.uid == this.author.id") {
50 post_update(
51 id: $postId,
52 data: { status: ARCHIVED }
53 )
54}
🔍 Pro Tip: ALWAYS use the @auth directive on mutations. level: USER means anyone logged in, expr: "auth.uid == this.author.id" means only the resource owner. Security must be considered from the ground up. Apply the principles from our iOS Security (in Turkish) article to the backend as well.

Auto-Generated SDKs

Data Connect's biggest advantage: it automatically generates iOS (Swift), Android (Kotlin), Flutter (Dart), and Web (TypeScript) SDKs from your query and mutation definitions.

Generating SDKs

bash
1# Generate SDKs
2firebase dataconnect:sdk:generate
3 
4# Output:
5# ├-- ios/ → Swift SDK
6# ├-- android/ → Kotlin SDK
7# ├-- dart/ → Flutter SDK
8# └-- web/ → TypeScript SDK

Using the TypeScript SDK

typescript
1// Auto-generated — DO NOT edit by hand
2import {
3 listPublishedPosts,
4 getPostBySlug,
5 createPost,
6 toggleLike,
7 ListPublishedPostsData,
8} from '@firebasegen/my-connector';
9 
10// Type-safe query
11const posts: ListPublishedPostsData = await listPublishedPosts({
12 limit: 10,
13 offset: 0,
14 categorySlug: 'typescript',
15});
16 
17// posts.posts[0].title → string (type-safe!)
18// posts.posts[0].author.displayName → string
19 
20// Type-safe mutation
21await createPost({
22 data: {
23 title: 'New Post',
24 slug: 'new-post',
25 content: 'Content...',
26 status: 'DRAFT',
27 // author automatic: auth.uid
28 },
29});

iOS / Swift Integration

Using the Swift SDK

swift
1import FirebaseDataConnect
2 
3// Auto-generated connector
4let connector = DataConnect.dataConnect(
5 connectorConfig: ConnectorConfig(
6 serviceId: "my-service",
7 location: "us-central1"
8 )
9)
10 
11// Type-safe query
12func loadPosts(category: String? = nil) async throws -> [Post] {
13 let result = try await ListPublishedPostsQuery
14 .ref(limit: 10, offset: 0, categorySlug: category)
15 .execute()
16 
17 return result.data.posts.map { post in
18 Post(
19 id: post.id,
20 title: post.title,
21 slug: post.slug,
22 excerpt: post.excerpt ?? "",
23 authorName: post.author.displayName,
24 categoryName: post.category?.name ?? "General"
25 )
26 }
27}
28 
29// Type-safe mutation
30func createNewPost(title: String, content: String) async throws {
31 try await CreatePostMutation
32 .ref(data: Post_Data(
33 title: title,
34 slug: title.slugified(),
35 content: content,
36 status: .draft
37 ))
38 .execute()
39}
40 
41// Toggle like
42func toggleLike(postId: UUID) async throws {
43 try await ToggleLikeMutation
44 .ref(postId: postId)
45 .execute()
46}

Flutter Integration

Using the Dart SDK

dart
1import 'package:firebase_data_connect/firebase_data_connect.dart';
2import 'package:my_connector/my_connector.dart';
3 
4class PostRepository {
5 final _connector = MyConnector.instance;
6 
7 // Get published posts
8 Future<List<Post>> getPublishedPosts({
9 int limit = 10,
10 int offset = 0,
11 String? categorySlug,
12 }) async {
13 final result = await _connector.listPublishedPosts
14 .ref(
15 limit: limit,
16 offset: offset,
17 categorySlug: categorySlug,
18 )
19 .execute();
20 
21 return result.data.posts.map((p) => Post(
22 id: p.id,
23 title: p.title,
24 slug: p.slug,
25 excerpt: p.excerpt ?? '',
26 authorName: p.author.displayName,
27 viewCount: p.viewCount,
28 )).toList();
29 }
30 
31 // Single post by slug
32 Future<PostDetail?> getPostBySlug(String slug) async {
33 final result = await _connector.getPostBySlug
34 .ref(slug: slug)
35 .execute();
36 
37 final posts = result.data.post;
38 if (posts.isEmpty) return null;
39 
40 final p = posts.first;
41 return PostDetail(
42 id: p.id,
43 title: p.title,
44 content: p.content,
45 author: Author(
46 name: p.author.displayName,
47 photo: p.author.photoUrl,
48 ),
49 comments: p.comments.map((c) => Comment(
50 content: c.content,
51 authorName: c.author.displayName,
52 createdAt: c.createdAt,
53 )).toList(),
54 );
55 }
56}
🔍 Pro Tip: Wrap the Data Connect SDK with the repository pattern from our Flutter Clean Architecture article. This way you can easily switch data sources (Firestore vs. Data Connect). The domain layer stays unaware of SDK details.

Authorization and Security

Data Connect offers a powerful authorization system integrated with Firebase Auth:

Auth Levels

Level
Description
Usage
PUBLIC
Accessible to everyone
Blog listing, product catalog
USER_ANON
All auth, including anonymous
Where rate limiting is needed
USER
Email-verified user
Writing comments, likes
USER_EMAIL_VERIFIED
Email verified
Sensitive operations
NO_ACCESS
Admin SDK only
Server-side operations

Fine-Grained Control with CEL Expressions

graphql
1# Only the resource owner can edit
2mutation UpdateProfile($data: User_Data!)
3 @auth(expr: "auth.uid == vars.data.id") {
4 user_update(data: $data)
5}
6 
7# Admin role check
8mutation DeletePost($postId: UUID!)
9 @auth(expr: "'admin' in auth.token.roles") {
10 post_delete(id: $postId)
11}
12 
13# Time-based check (last 24 hours)
14mutation EditComment($commentId: UUID!, $content: String!)
15 @auth(expr: "auth.uid == this.author.id && (request.time - this.createdAt) < duration('24h')") {
16 comment_update(id: $commentId, data: { content: $content })
17}

Firestore vs. Data Connect

As someone who uses both services in production, let's compare them:

Criterion
Firestore
Data Connect
Data Model
NoSQL (Document)
Relational (SQL)
Schema
Schema-less
Schema-first (GraphQL)
Query
Collection query
SQL-strength GraphQL
JOIN
None (denormalization)
Native JOIN
Aggregation
count() (limited)
SUM, AVG, COUNT, GROUP BY
Real-time
Snapshot listener
Still limited
Type Safety
Weak
Full (auto-generated SDK)
Offline
Strong cache
Not yet available
Migration
Manual
SQL migration
Pricing
Per read/write/delete
Cloud SQL instance + query
Scaling
Automatic
Adjust instance size

When to Use Which?

  • Firestore: Real-time sync, offline-first, simple data model, fast prototyping
  • Data Connect: Relational data, complex queries, type safety, a team with SQL knowledge

Production Best Practices

1. Schema Migration

bash
1# After making a schema change
2firebase dataconnect:sql:migrate --force
3 
4# To see the migration SQL
5firebase dataconnect:sql:diff

2. Query Optimization

graphql
1# BAD: fetch all fields (over-fetching)
2query GetPosts @auth(level: PUBLIC) {
3 posts {
4 id title slug content excerpt status
5 publishedAt viewCount createdAt updatedAt
6 author { id displayName email photoUrl bio }
7 comments { id content author { id displayName } }
8 }
9}
10 
11# GOOD: only the fields you need
12query GetPostCards @auth(level: PUBLIC) {
13 posts(
14 where: { status: { eq: PUBLISHED } }
15 limit: 10
16 ) {
17 id title slug excerpt
18 author { displayName }
19 category { name }
20 }
21}

3. Index Strategy

graphql
1# Define an index in the schema
2type Post @table @index(fields: ["status", "publishedAt"]) {
3 # ...
4}
5 
6# Composite index
7type Comment @table @index(fields: ["post", "createdAt"]) {
8 # ...
9}

Conclusion and Recommendations

Firebase Data Connect fills the Firebase ecosystem's biggest gap: relational data management. With its GraphQL schema-first approach, auto-generated type-safe SDKs, and the power of Cloud SQL, it's a strong option for production applications. The GraphQL best practices from our GraphQL Mobile article apply here too. Combine it with Firebase Advanced (in Turkish) to use the full power of the Firebase ecosystem.

Recommendations

  1. Relational data? Use Data Connect — trying to do JOINs in Firestore is wasted effort
  2. Think schema-first — design the data model first, then the UI
  3. Wrap the SDK with the repository pattern — be ready for data source changes. Strengthen it with the concurrency patterns from our What's New in Swift 6 (in Turkish) article
  4. Put auth directives everywhere — security should be the default
  5. Use it alongside Firestore — Firestore for real-time, Data Connect for relational data

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

Reader Reward

The Data Connect + Firestore hybrid pattern: keep relational data (users, orders, products) in Data Connect, and keep real-time needs (chat messages, online status, notifications) in Firestore. Write a Cloud Function that bridges the two services — when an order is created in Data Connect, write a real-time notification to Firestore. This hybrid approach gives you the best of both worlds.

Tags

#Firebase#GraphQL#PostgreSQL#Cloud SQL#type safety#SDK#backend
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share