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?
- Architecture Overview
- Why Data Connect?
- Setup
- Schema-First Approach
- GraphQL Schema + Query Quick Start
- Schema Definition
- Schema Directives
- Defining Queries and Mutations
- Query Definitions
- Mutation Definitions
- Auto-Generated SDKs
- Generating SDKs
- Using the TypeScript SDK
- iOS / Swift Integration
- Using the Swift SDK
- Flutter Integration
- Using the Dart SDK
- Authorization and Security
- Auth Levels
- Fine-Grained Control with CEL Expressions
- Firestore vs. Data Connect
- When to Use Which?
- Production Best Practices
- 1. Schema Migration
- 2. Query Optimization
- 3. Index Strategy
- Conclusion and Recommendations
- Recommendations
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
1Mobile/Web App2 ↓ (Type-safe SDK)3Firebase Data Connect Service4 ↓ (GraphQL)5Cloud SQL for PostgreSQLWhy 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
1# Update Firebase CLI2npm install -g firebase-tools@latest3 4# Initialize Data Connect5firebase init dataconnect6 7# Project structure is created:8# dataconnect/9# ├-- schema/ → GraphQL schema files10# ├-- connector/ → Query and mutation definitions11# └-- 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
1# dataconnect/schema/schema.gql — minimal blog schema2type Post @table {3 id: UUID! @default(expr: "uuidV4()")4 title: String!5 slug: String! @unique6 content: String!7 status: PostStatus! @default(value: "DRAFT")8 author: User! @relation9 publishedAt: Timestamp10 createdAt: Timestamp! @default(expr: "request.time")11}12 13enum PostStatus { DRAFT PUBLISHED ARCHIVED }14 15# connector/queries.gql — list published posts16query ListPosts($limit: Int! = 10) @auth(level: PUBLIC) {17 posts(18 where: { status: { eq: PUBLISHED } }19 orderBy: [{ publishedAt: DESC }]20 limit: $limit21 ) {22 id title slug publishedAt23 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
1# dataconnect/schema/schema.gql2 3# User table4type User @table {5 id: UUID! @default(expr: "uuidV4()")6 email: String! @unique7 displayName: String!8 photoUrl: String9 bio: String10 createdAt: Timestamp! @default(expr: "request.time")11 updatedAt: Timestamp! @default(expr: "request.time")12}13 14# Blog post15type Post @table {16 id: UUID! @default(expr: "uuidV4()")17 title: String!18 slug: String! @unique19 content: String!20 excerpt: String21 status: PostStatus! @default(value: "DRAFT")22 publishedAt: Timestamp23 author: User! @relation24 category: Category @relation25 viewCount: Int! @default(value: 0)26 createdAt: Timestamp! @default(expr: "request.time")27}28 29# Category30type Category @table {31 id: UUID! @default(expr: "uuidV4()")32 name: String! @unique33 slug: String! @unique34 description: String35 posts: [Post!]! @relation # Reverse relation36}37 38# Comment39type Comment @table {40 id: UUID! @default(expr: "uuidV4()")41 content: String!42 author: User! @relation43 post: Post! @relation44 parentComment: Comment @relation # Nested comments45 createdAt: Timestamp! @default(expr: "request.time")46}47 48# Like (many-to-many)49type Like @table(50 key: ["user", "post"] # Composite primary key51) {52 user: User! @relation53 post: Post! @relation54 createdAt: Timestamp! @default(expr: "request.time")55}56 57# Enum58enum PostStatus {59 DRAFT60 PUBLISHED61 ARCHIVED62}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
1# dataconnect/connector/queries.gql2 3# Get all published posts (with pagination)4query ListPublishedPosts(5 $limit: Int! = 10,6 $offset: Int! = 0,7 $categorySlug: String8) @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: $offset17 ) {18 id19 title20 slug21 excerpt22 publishedAt23 viewCount24 author {25 displayName26 photoUrl27 }28 category {29 name30 slug31 }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 id39 title40 slug41 content42 publishedAt43 viewCount44 author {45 id46 displayName47 photoUrl48 bio49 }50 category {51 name52 slug53 }54 comments(orderBy: [{ createdAt: DESC }]) {55 id56 content57 createdAt58 author {59 displayName60 photoUrl61 }62 }63 }64}65 66# User profile and stats67query GetUserProfile($userId: UUID!) @auth(level: USER) {68 user(id: $userId) {69 id70 displayName71 email72 bio73 photoUrl74 posts_on_author(where: { status: { eq: PUBLISHED } }) {75 id76 title77 viewCount78 }79 comments_on_author {80 id81 }82 likes_on_user {83 post {84 id85 title86 }87 }88 }89}Mutation Definitions
1# dataconnect/connector/mutations.gql2 3# Create a new post4mutation 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: PostStatus14) @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 comment27mutation AddComment(28 $postId: UUID!,29 $content: String!,30 $parentCommentId: UUID31) @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 like41mutation 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@authdirective on mutations.level: USERmeans 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
1# Generate SDKs2firebase dataconnect:sdk:generate3 4# Output:5# ├-- ios/ → Swift SDK6# ├-- android/ → Kotlin SDK7# ├-- dart/ → Flutter SDK8# └-- web/ → TypeScript SDKUsing the TypeScript SDK
1// Auto-generated — DO NOT edit by hand2import {3 listPublishedPosts,4 getPostBySlug,5 createPost,6 toggleLike,7 ListPublishedPostsData,8} from '@firebasegen/my-connector';9 10// Type-safe query11const 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 → string19 20// Type-safe mutation21await createPost({22 data: {23 title: 'New Post',24 slug: 'new-post',25 content: 'Content...',26 status: 'DRAFT',27 // author automatic: auth.uid28 },29});iOS / Swift Integration
Using the Swift SDK
1import FirebaseDataConnect2 3// Auto-generated connector4let connector = DataConnect.dataConnect(5 connectorConfig: ConnectorConfig(6 serviceId: "my-service",7 location: "us-central1"8 )9)10 11// Type-safe query12func loadPosts(category: String? = nil) async throws -> [Post] {13 let result = try await ListPublishedPostsQuery14 .ref(limit: 10, offset: 0, categorySlug: category)15 .execute()16 17 return result.data.posts.map { post in18 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 mutation30func createNewPost(title: String, content: String) async throws {31 try await CreatePostMutation32 .ref(data: Post_Data(33 title: title,34 slug: title.slugified(),35 content: content,36 status: .draft37 ))38 .execute()39}40 41// Toggle like42func toggleLike(postId: UUID) async throws {43 try await ToggleLikeMutation44 .ref(postId: postId)45 .execute()46}Flutter Integration
Using the Dart SDK
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 posts8 Future<List<Post>> getPublishedPosts({9 int limit = 10,10 int offset = 0,11 String? categorySlug,12 }) async {13 final result = await _connector.listPublishedPosts14 .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 slug32 Future<PostDetail?> getPostBySlug(String slug) async {33 final result = await _connector.getPostBySlug34 .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
1# Only the resource owner can edit2mutation UpdateProfile($data: User_Data!)3 @auth(expr: "auth.uid == vars.data.id") {4 user_update(data: $data)5}6 7# Admin role check8mutation 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
1# After making a schema change2firebase dataconnect:sql:migrate --force3 4# To see the migration SQL5firebase dataconnect:sql:diff2. Query Optimization
1# BAD: fetch all fields (over-fetching)2query GetPosts @auth(level: PUBLIC) {3 posts {4 id title slug content excerpt status5 publishedAt viewCount createdAt updatedAt6 author { id displayName email photoUrl bio }7 comments { id content author { id displayName } }8 }9}10 11# GOOD: only the fields you need12query GetPostCards @auth(level: PUBLIC) {13 posts(14 where: { status: { eq: PUBLISHED } }15 limit: 1016 ) {17 id title slug excerpt18 author { displayName }19 category { name }20 }21}3. Index Strategy
1# Define an index in the schema2type Post @table @index(fields: ["status", "publishedAt"]) {3 # ...4}5 6# Composite index7type 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
- Relational data? Use Data Connect — trying to do JOINs in Firestore is wasted effort
- Think schema-first — design the data model first, then the UI
- 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
- Put auth directives everywhere — security should be the default
- 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
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.

