NYC
skills/firebase/firebase-tools/firebase-data-connect

firebase-data-connect

SKILL.md

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

Weekly Installs
8
First Seen
13 days ago
Installed on
gemini-cli8
opencode5
codex5
github-copilot5
antigravity5
amp3