Firebase Data Connect
Expert guidance for integrating Firebase Data Connect with Angular applications, providing type-safe GraphQL operations backed by PostgreSQL.
When to Use This Skill
Activate this skill when you need to:
-
Define GraphQL schemas for Firebase Data Connect
-
Create queries and mutations for data operations
-
Integrate Data Connect with Angular services
-
Generate type-safe TypeScript SDKs
-
Implement real-time data subscriptions
-
Configure Data Connect connectors and services
-
Migrate from Firestore to Data Connect
-
Optimize GraphQL query performance
What is Firebase Data Connect?
Firebase Data Connect is a relational database service that provides:
-
GraphQL API for type-safe data access
-
PostgreSQL backend for relational data
-
Generated SDKs for TypeScript/JavaScript
-
Real-time subscriptions for live data
-
Server-side validation and security rules
-
Local emulator for development and testing
Prerequisites
Required Tools
-
Firebase CLI (npm install -g firebase-tools )
-
Node.js 18+ and npm/pnpm
-
Angular 20+ project
-
Firebase project with Data Connect enabled
Installation
Install Firebase CLI
npm install -g firebase-tools
Login to Firebase
firebase login
Initialize Data Connect in your project
firebase init dataconnect
Install Angular Fire (if not already installed)
npm install @angular/fire
Project Structure
dataconnect/ ├── dataconnect.yaml # Connector configuration ├── schema/ │ ├── schema.gql # GraphQL schema definitions │ └── types.gql # Custom types and enums ├── queries/ │ ├── users.gql # User queries │ └── workspace.gql # Workspace queries ├── mutations/ │ ├── createUser.gql # User mutations │ └── updateWorkspace.gql # Workspace mutations └── seed_data.gql # Development seed data
src/dataconnect-generated/ # Generated TypeScript SDK └── index.ts # Auto-generated type-safe operations
Schema Definition
Basic Schema Example
dataconnect/schema/schema.gql
type User @table { id: UUID! @default(expr: "uuidV4()") email: String! @unique displayName: String! photoURL: String createdAt: Timestamp! @default(expr: "request.time") updatedAt: Timestamp! @default(expr: "request.time")
Relationships
workspaces: [WorkspaceMember!]! @relationFrom(field: "user") }
type Workspace @table { id: UUID! @default(expr: "uuidV4()") name: String! description: String ownerId: UUID! createdAt: Timestamp! @default(expr: "request.time")
Relationships
owner: User! @relation(fields: "ownerId") members: [WorkspaceMember!]! @relationFrom(field: "workspace") }
type WorkspaceMember @table { id: UUID! @default(expr: "uuidV4()") userId: UUID! workspaceId: UUID! role: MemberRole! joinedAt: Timestamp! @default(expr: "request.time")
Relationships
user: User! @relation(fields: "userId") workspace: Workspace! @relation(fields: "workspaceId")
Composite unique constraint
@unique(fields: ["userId", "workspaceId"]) }
enum MemberRole { OWNER ADMIN MEMBER VIEWER }
Queries
Query Definition
dataconnect/queries/users.gql
query GetUser($userId: UUID!) @auth(level: USER) { user(id: $userId) { id email displayName photoURL createdAt } }
query ListUserWorkspaces($userId: UUID!) @auth(level: USER) { workspaceMembers(where: { userId: { eq: $userId } }) { workspace { id name description owner { id displayName } } role joinedAt } }
query SearchWorkspaces($searchTerm: String!) @auth(level: USER) { workspaces( where: { name: { contains: $searchTerm } } orderBy: { createdAt: DESC } limit: 20 ) { id name description owner { displayName } createdAt } }
Mutations
Mutation Definition
dataconnect/mutations/workspace.gql
mutation CreateWorkspace( $name: String! $description: String $ownerId: UUID! ) @auth(level: USER) { workspace_insert(data: { name: $name description: $description ownerId: $ownerId }) { id name description createdAt } }
mutation AddWorkspaceMember( $workspaceId: UUID! $userId: UUID! $role: MemberRole! ) @auth(level: USER) { workspaceMember_insert(data: { workspaceId: $workspaceId userId: $userId role: $role }) { id user { displayName email } role joinedAt } }
mutation UpdateWorkspace( $id: UUID! $name: String $description: String ) @auth(level: USER) { workspace_update( id: $id data: { name: $name description: $description } ) { id name description updatedAt } }
Angular Integration
Generated SDK Usage
// src/app/infrastructure/firebase/data-connect.service.ts import { Injectable, inject } from '@angular/core'; import { ConnectorConfig, getDataConnect } from '@angular/fire/data-connect'; import { getUser, listUserWorkspaces, createWorkspace, addWorkspaceMember } from '@/dataconnect-generated';
@Injectable({ providedIn: 'root' }) export class DataConnectService { private dataConnect = getDataConnect();
// Execute query async getUserById(userId: string) { const result = await getUser(this.dataConnect, { userId }); return result.data.user; }
// Execute query with variables async getUserWorkspaces(userId: string) { const result = await listUserWorkspaces(this.dataConnect, { userId }); return result.data.workspaceMembers; }
// Execute mutation async createNewWorkspace(name: string, description: string, ownerId: string) { const result = await createWorkspace(this.dataConnect, { name, description, ownerId }); return result.data.workspace_insert; } }
Repository Pattern
// src/app/infrastructure/persistence/workspace-dataconnect.repository.ts import { Injectable } from '@angular/core'; import { Observable, from } from 'rxjs'; import { map } from 'rxjs/operators'; import { DataConnectService } from '../firebase/data-connect.service'; import { IWorkspaceRepository } from '@domain/repositories/workspace.repository'; import { Workspace } from '@domain/workspace/workspace.entity';
@Injectable({ providedIn: 'root' }) export class WorkspaceDataConnectRepository implements IWorkspaceRepository { constructor(private dataConnect: DataConnectService) {}
findById(id: string): Observable<Workspace | null> { return from(this.dataConnect.getWorkspaceById(id)).pipe( map(data => data ? this.toDomain(data) : null) ); }
findByOwnerId(ownerId: string): Observable<Workspace[]> { return from(this.dataConnect.getUserWorkspaces(ownerId)).pipe( map(members => members.map(m => this.toDomain(m.workspace))) ); }
save(workspace: Workspace): Observable<Workspace> { return from(this.dataConnect.createNewWorkspace( workspace.name, workspace.description, workspace.ownerId )).pipe( map(data => this.toDomain(data)) ); }
private toDomain(data: any): Workspace { // Map Data Connect response to domain entity return new Workspace({ id: data.id, name: data.name, description: data.description, ownerId: data.ownerId, createdAt: new Date(data.createdAt) }); } }
NgRx Signals Integration
// src/app/application/store/workspace.store.ts import { signalStore, withState, withMethods } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { pipe, switchMap, tap } from 'rxjs'; import { tapResponse } from '@ngrx/operators'; import { inject } from '@angular/core'; import { patchState } from '@ngrx/signals'; import { DataConnectService } from '@infrastructure/firebase/data-connect.service';
export const WorkspaceStore = signalStore( { providedIn: 'root' }, withState({ workspaces: [] as Workspace[], selectedWorkspace: null as Workspace | null, loading: false, error: null as string | null }), withMethods((store, dataConnect = inject(DataConnectService)) => ({ loadUserWorkspaces: rxMethod<string>( pipe( tap(() => patchState(store, { loading: true, error: null })), switchMap((userId) => dataConnect.getUserWorkspaces(userId)), tapResponse({ next: (workspaces) => patchState(store, { workspaces, loading: false }), error: (error: Error) => patchState(store, { error: error.message, loading: false }) }) ) ),
createWorkspace: rxMethod<{ name: string; description: string; ownerId: string }>(
pipe(
tap(() => patchState(store, { loading: true })),
switchMap(({ name, description, ownerId }) =>
dataConnect.createNewWorkspace(name, description, ownerId)
),
tapResponse({
next: (workspace) => patchState(store, (state) => ({
workspaces: [...state.workspaces, workspace],
loading: false
})),
error: (error: Error) => patchState(store, {
error: error.message,
loading: false
})
})
)
)
})) );
Configuration
dataconnect.yaml
dataconnect/dataconnect.yaml
connectorId: my-connector cloudSql: instanceId: my-instance database: my-database schema: source: ./schema datasource: postgresql: {} queries: source: ./queries mutations: source: ./mutations
Angular Fire Configuration
// src/app/app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideFirebaseApp, initializeApp } from '@angular/fire/app'; import { provideDataConnect, getDataConnect } from '@angular/fire/data-connect'; import { environment } from './environments/environment';
export const appConfig: ApplicationConfig = { providers: [ provideFirebaseApp(() => initializeApp(environment.firebase)), provideDataConnect(() => getDataConnect({ connector: 'my-connector', location: 'us-central1' })), ] };
Local Development
Start Emulator
Start Data Connect emulator
firebase emulators:start --only dataconnect
Run with seed data
firebase emulators:start --only dataconnect --import=./seed-data
Generate TypeScript SDK
firebase dataconnect:sdk:generate --output=src/dataconnect-generated
Seed Data
dataconnect/seed_data.gql
mutation SeedUsers { user1: user_insert(data: { email: "admin@example.com" displayName: "Admin User" }) { id }
user2: user_insert(data: { email: "member@example.com" displayName: "Member User" }) { id } }
mutation SeedWorkspaces { workspace1: workspace_insert(data: { name: "Default Workspace" description: "Main workspace" ownerId: "USER_ID_HERE" }) { id } }
Best Practices
Schema Design
// ✅ Good - Use proper relationships type Post @table { authorId: UUID! author: User! @relation(fields: "authorId") }
// ❌ Bad - Duplicating data type Post @table { authorEmail: String authorName: String }
Query Optimization
✅ Good - Request only needed fields
query GetWorkspace($id: UUID!) { workspace(id: $id) { id name owner { displayName } } }
❌ Bad - Over-fetching
query GetWorkspace($id: UUID!) { workspace(id: $id) { id name description owner { id email displayName photoURL createdAt updatedAt } members { user { id email displayName } } } }
Error Handling
// ✅ Good - Handle errors properly async loadWorkspace(id: string) { try { const result = await getWorkspace(this.dataConnect, { id }); if (result.errors) { throw new Error(result.errors[0].message); } return result.data.workspace; } catch (error) { console.error('Failed to load workspace:', error); throw error; } }
// ❌ Bad - Ignore errors async loadWorkspace(id: string) { const result = await getWorkspace(this.dataConnect, { id }); return result.data.workspace; }
Security
Authentication Rules
Require authentication
query GetUser($id: UUID!) @auth(level: USER) { user(id: $id) { ... } }
Require specific role
mutation DeleteWorkspace($id: UUID!) @auth(level: USER, expr: "auth.uid == resource.ownerId") { workspace_delete(id: $id) { id } }
Input Validation
Use constraints
type User @table { email: String! @unique displayName: String! @check(expr: "length(this) >= 3") age: Int @check(expr: "this >= 18") }
Troubleshooting
Issue Cause Solution
SDK generation fails Schema syntax errors Run firebase dataconnect:validate
Query returns null Missing @auth directive Add proper authentication
Relationship error Incorrect field references Check @relation fields match column names
Emulator won't start Port already in use Change port in firebase.json or kill process
Type mismatch Stale generated code Re-run firebase dataconnect:sdk:generate
Migration from Firestore
// Before (Firestore) const docRef = doc(firestore, 'workspaces', id); const docSnap = await getDoc(docRef); const data = docSnap.data();
// After (Data Connect) const result = await getWorkspace(dataConnect, { id }); const data = result.data.workspace;
References
-
Firebase Data Connect Documentation
-
GraphQL Schema Reference
-
AngularFire Data Connect
-
PostgreSQL Data Types