Client-side SDK for interacting with authentication and authorization cloud functions.
npm install auth-cloud-sdk
┌─────────────────┐ ┌───────────────┐ ┌─────────────────────┐
│ │ │ │ │ │
│ Client App │──────▶ Auth Cloud │──────▶ Google Cloud │
│ (React, Vue, │ │ SDK │ │ Functions │
│ Angular, etc) │◀─────│ │◀─────│ │
│ │ │ │ │ │
└─────────────────┘ └───────────────┘ └─────────────────────┘
│
│
▼
┌─────────────────────┐
│ │
│ Firebase Auth │
│ Firestore │
│ SpiceDB │
│ │
└─────────────────────┘
`
Getting Started
$3
Install the Auth Cloud SDK using npm or yarn:
`bash
npm install auth-cloud-sdk
or
yarn add auth-cloud-sdk
`
$3
Before using the Auth Cloud SDK, ensure you have:
1. Firebase Initialized: Your client application must initialize Firebase, particularly Firebase Authentication.
`typescript
// Example Firebase initialization
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
`
2. Deployed Cloud Functions: The following Google Cloud Functions must be deployed:
- executeAuthorizedFirestoreOperation
- manageSpiceDBRelationship
- writeSpiceDBSchema
- lookupAuthorizedResources
3. SpiceDB Instance: A running SpiceDB instance configured with your authorization schema.
$3
Before using any SDK functions, configure it with your Cloud Function endpoints:
`typescript
import { setCloudFunctionEndpoints } from 'auth-cloud-sdk';
setCloudFunctionEndpoints({
executeAuthorizedFirestoreOperation: 'https://your-region-your-project.cloudfunctions.net/executeAuthorizedFirestoreOperation',
manageSpiceDBRelationship: 'https://your-region-your-project.cloudfunctions.net/manageSpiceDBRelationship',
writeSpiceDBSchema: 'https://your-region-your-project.cloudfunctions.net/writeSpiceDBSchema',
lookupAuthorizedResources: 'https://your-region-your-project.cloudfunctions.net/lookupAuthorizedResources'
});
`
$3
Here's a simple example of reading a document with authorization checks:
`typescript
import { executeAuthorizedFirestoreOperation, Types } from 'auth-cloud-sdk';
async function getSecureDocument(documentId) {
try {
// Define authorization parameters
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: documentId,
permission: 'read'
};
// Define Firestore operation
const firestoreOperation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT',
collectionPath: 'secureDocuments',
documentId: documentId
};
// Execute the operation with authorization check
const document = await executeAuthorizedFirestoreOperation(
authorization,
firestoreOperation
);
return document;
} catch (error) {
console.error('Error fetching document:', error);
throw error;
}
}
`
Core Concepts
$3
The Auth Cloud SDK relies on Firebase Authentication for user identity. When you call any SDK function, it automatically:
1. Retrieves the current authenticated user from Firebase Auth
2. Gets a fresh ID token for that user
3. Sends this token to the Cloud Function for verification
This ensures that all operations are performed in the context of an authenticated user. If no user is signed in, the SDK will throw an error.
$3
The SDK uses SpiceDB (formerly Authzed) as its authorization system. SpiceDB implements a relationship-based authorization model where:
- Resources are objects that users can access (documents, collections, features)
- Relations define how subjects relate to resources (viewer, editor, owner)
- Subjects are typically users, but can also be groups or other entities
- Permissions are derived from relations through a schema
The SDK provides functions to:
- Create, update, or delete relationships
- Check if a user has permission on a resource
- Find all resources a user has permission to access
- Update the authorization schema
$3
The SDK combines Firestore operations with authorization checks in a single call. This ensures that users can only perform operations they're authorized for. Supported operations include:
- Reading documents
- Querying collections
- Creating documents
- Updating documents
- Deleting documents
Each operation is checked against the user's permissions before execution.
$3
The SDK allows you to manage authorization relationships in SpiceDB. This includes:
- Creating new relationships (granting permissions)
- Touching relationships (updating timestamps)
- Deleting relationships (revoking permissions)
API Reference
$3
#### setCloudFunctionEndpoints(endpoints: CloudFunctionEndpoints): void
Configures the SDK with the necessary Cloud Function endpoint URLs. This must be called once before using any SDK functions.
Parameters:
- endpoints: An object containing the URLs for the Cloud Functions.
- executeAuthorizedFirestoreOperation: URL for the Firestore operations function
- manageSpiceDBRelationship: URL for the relationship management function
- writeSpiceDBSchema: URL for the schema writing function
- lookupAuthorizedResources: URL for the resource lookup function
Throws:
- Error if endpoints configuration is empty
- Error if any required endpoint is missing
Example:
`typescript
import { setCloudFunctionEndpoints } from 'auth-cloud-sdk';
setCloudFunctionEndpoints({
executeAuthorizedFirestoreOperation: 'https://us-central1-myproject.cloudfunctions.net/executeAuthorizedFirestoreOperation',
manageSpiceDBRelationship: 'https://us-central1-myproject.cloudfunctions.net/manageSpiceDBRelationship',
writeSpiceDBSchema: 'https://us-central1-myproject.cloudfunctions.net/writeSpiceDBSchema',
lookupAuthorizedResources: 'https://us-central1-myproject.cloudfunctions.net/lookupAuthorizedResources'
});
`
#### getCloudFunctionEndpoints(): CloudFunctionEndpoints
Retrieves the configured Cloud Function endpoints.
Returns:
- An object containing the configured endpoint URLs
Throws:
- Error if endpoints have not been set
Example:
`typescript
import { getCloudFunctionEndpoints } from 'auth-cloud-sdk';
try {
const endpoints = getCloudFunctionEndpoints();
console.log('Configured endpoints:', endpoints);
} catch (error) {
console.error('Endpoints not configured:', error);
}
`
$3
#### executeAuthorizedFirestoreOperation(authorization: Authorization, firestoreOperation: FirestoreOperation): Promise
Executes a Firestore operation through the designated Cloud Function, with authorization checks.
Parameters:
- authorization: Authorization details for the operation
- resourceObjectType: Type of resource being accessed
- resourceObjectId: ID of the resource being accessed
- permission: Permission required for the operation
- subjectObjectType: (Optional) Type of the subject, defaults to 'user'
- zedToken: (Optional) ZedToken for consistency
- firestoreOperation: The Firestore operation to perform
- operationType: Type of operation ('GET_DOCUMENT', 'GET_COLLECTION', 'CREATE_DOCUMENT', 'UPDATE_DOCUMENT', 'DELETE_DOCUMENT')
- collectionPath: Path to the Firestore collection
- documentId: (Optional) ID of the document
- data: (Optional) Data for create/update operations
- queryConstraints: (Optional) Query constraints for collection queries
Returns:
- A promise that resolves with the result of the Firestore operation
Throws:
- Error if the user is not authenticated
- Error if the Cloud Function call fails
- Error if the user lacks the required permission
Example:
`typescript
import { executeAuthorizedFirestoreOperation, Types } from 'auth-cloud-sdk';
// Read a document
async function readDocument(docId) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: docId,
permission: 'read'
};
const operation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT',
collectionPath: 'documents',
documentId: docId
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Query a collection
async function queryDocuments() {
const authorization: Types.Authorization = {
resourceObjectType: 'collection',
resourceObjectId: 'documents',
permission: 'list'
};
const operation: Types.FirestoreOperation = {
operationType: 'GET_COLLECTION',
collectionPath: 'documents',
queryConstraints: [
{ field: 'status', op: '==', value: 'published' },
{ orderBy: 'createdAt', direction: 'desc' },
{ limit: 10 }
]
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Create a document
async function createDocument(data) {
const authorization: Types.Authorization = {
resourceObjectType: 'collection',
resourceObjectId: 'documents',
permission: 'create'
};
const operation: Types.FirestoreOperation = {
operationType: 'CREATE_DOCUMENT',
collectionPath: 'documents',
data: data
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Update a document
async function updateDocument(docId, data) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: docId,
permission: 'update'
};
const operation: Types.FirestoreOperation = {
operationType: 'UPDATE_DOCUMENT',
collectionPath: 'documents',
documentId: docId,
data: data
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Delete a document
async function deleteDocument(docId) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: docId,
permission: 'delete'
};
const operation: Types.FirestoreOperation = {
operationType: 'DELETE_DOCUMENT',
collectionPath: 'documents',
documentId: docId
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
`
$3
#### manageSpiceDBRelationship(payload: ManageRelationshipPayload): Promise
Manages SpiceDB relationships (create, touch, delete) through the Cloud Function.
Parameters:
- payload: The payload containing the operation type and relationships
- operation: Type of operation ('CREATE', 'TOUCH', 'DELETE')
- relationships: Array of relationship objects
- resource: The resource object (type and ID)
- relation: The relation name
- subject: The subject object (type and ID)
Returns:
- A promise that resolves with the result of the operation
- status: Operation status ('success')
- operation: The operation that was performed
- zedToken: ZedToken for consistency
Throws:
- Error if the user is not authenticated
- Error if the Cloud Function call fails
Example:
`typescript
import { manageSpiceDBRelationship, Types } from 'auth-cloud-sdk';
// Grant a user viewer access to a document
async function grantViewAccess(userId, documentId) {
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: 'viewer',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Grant a user editor access to a document
async function grantEditAccess(userId, documentId) {
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: 'editor',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Revoke a user's access to a document
async function revokeAccess(userId, documentId, relation) {
const payload: Types.ManageRelationshipPayload = {
operation: 'DELETE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: relation, // 'viewer', 'editor', etc.
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
`
#### writeSpiceDBSchema(schemaText: string): Promise
Writes a new schema to SpiceDB through the Cloud Function. This operation is typically restricted to super admins.
Parameters:
- schemaText: The SpiceDB schema definition text
Returns:
- A promise that resolves with the result of the operation
- status: Operation status ('success')
- message: Success message
Throws:
- Error if the user is not authenticated
- Error if the Cloud Function call fails
- Error if the user is not authorized to write schemas
Example:
`typescript
import { writeSpiceDBSchema } from 'auth-cloud-sdk';
// Update the authorization schema
async function updateAuthSchema() {
const schemaText =
;
return writeSpiceDBSchema(schemaText);
}
`
#### lookupAuthorizedResources(resourceObjectType: string, permission: string, subjectObjectType?: string, zedToken?: string): Promise
Looks up resources a subject has a given permission on, via the Cloud Function.
Parameters:
- resourceObjectType: The type of resource to look up (e.g., "document", "folder")
- permission: The permission to check (e.g., "view", "edit")
- subjectObjectType: (Optional) The type of the subject (e.g., "user"). Defaults to "user" if undefined
- zedToken: (Optional) ZedToken for consistency
Returns:
- A promise that resolves with a list of resource IDs
- resourceIds: Array of resource IDs the user has permission on
Throws:
- Error if the user is not authenticated
- Error if the Cloud Function call fails
Example:
`typescript
import { lookupAuthorizedResources } from 'auth-cloud-sdk';
// Find all documents the user can view
async function findViewableDocuments() {
const result = await lookupAuthorizedResources('document', 'view');
return result.resourceIds;
}
// Find all documents the user can edit
async function findEditableDocuments() {
const result = await lookupAuthorizedResources('document', 'edit');
return result.resourceIds;
}
// Find all collections the user can create documents in
async function findWritableCollections() {
const result = await lookupAuthorizedResources('collection', 'create');
return result.resourceIds;
}
`
Type Definitions
The SDK exports the following TypeScript interfaces:
$3
Defines the structure for the Cloud Function endpoints configuration.
`typescript
interface CloudFunctionEndpoints {
executeAuthorizedFirestoreOperation: string;
manageSpiceDBRelationship: string;
writeSpiceDBSchema: string;
lookupAuthorizedResources: string;
}
`
$3
Defines the structure for a Firestore operation.
`typescript
interface FirestoreOperation {
operationType: 'GET_DOCUMENT' | 'GET_COLLECTION' | 'CREATE_DOCUMENT' | 'UPDATE_DOCUMENT' | 'DELETE_DOCUMENT';
collectionPath: string;
documentId?: string;
data?: any;
queryConstraints?: Array<{
field?: string;
op?: WhereFilterOp;
value?: any;
orderBy?: string;
direction?: 'asc' | 'desc';
limit?: number;
}>;
}
`
$3
Defines the authorization details required for an operation.
`typescript
interface Authorization {
subjectObjectType?: string; // Defaults to 'user' in the Cloud Function if not provided
resourceObjectType: string;
resourceObjectId: string;
permission: string;
zedToken?: string; // Optional ZedToken for consistency
}
`
$3
Defines the structure for a single relationship to be managed in SpiceDB.
`typescript
interface Relationship {
resource: {
objectType: string;
objectId: string;
};
relation: string;
subject: {
object: {
objectType?: string; // Defaults to 'user' in the Cloud Function if not provided
objectId: string;
};
optionalRelation?: string;
};
}
`
$3
Defines the payload for managing SpiceDB relationships.
`typescript
interface ManageRelationshipPayload {
operation: 'CREATE' | 'TOUCH' | 'DELETE';
relationships: Relationship[];
}
`
$3
Defines the response structure for a successful relationship management operation.
`typescript
interface ManageRelationshipResponse {
status: 'success';
operation: 'CREATE' | 'TOUCH' | 'DELETE';
zedToken: string;
}
`
$3
Defines the response structure for a successful schema write operation.
`typescript
interface WriteSchemaResponse {
status: 'success';
message: string;
}
`
$3
Defines the response structure for looking up authorized resources.
`typescript
interface LookupAuthorizedResourcesResponse {
resourceIds: string[];
}
`
$3
Generic error response structure from Cloud Functions.
`typescript
interface CloudFunctionErrorResponse {
error: string;
}
`
Usage Examples
$3
`typescript
import {
executeAuthorizedFirestoreOperation,
manageSpiceDBRelationship,
lookupAuthorizedResources,
Types
} from 'auth-cloud-sdk';
class SecureDocumentManager {
// Create a new document with the current user as owner
async createDocument(title, content) {
const currentUser = firebase.auth().currentUser;
if (!currentUser) throw new Error('User not authenticated');
// First, create the document
const authorization: Types.Authorization = {
resourceObjectType: 'collection',
resourceObjectId: 'documents',
permission: 'create'
};
const createOperation: Types.FirestoreOperation = {
operationType: 'CREATE_DOCUMENT',
collectionPath: 'documents',
data: {
title,
content,
createdBy: currentUser.uid,
createdAt: new Date().toISOString()
}
};
const newDoc = await executeAuthorizedFirestoreOperation(authorization, createOperation);
// Then, set the current user as the owner
const relationshipPayload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: newDoc.id
},
relation: 'owner',
subject: {
object: {
objectType: 'user',
objectId: currentUser.uid
}
}
}
]
};
await manageSpiceDBRelationship(relationshipPayload);
return newDoc;
}
// Get all documents the user can view
async getViewableDocuments() {
// First, get all document IDs the user can view
const lookupResult = await lookupAuthorizedResources('document', 'view');
const documentIds = lookupResult.resourceIds;
if (documentIds.length === 0) {
return [];
}
// Then, fetch the documents in batches
const documents = [];
const batchSize = 10;
for (let i = 0; i < documentIds.length; i += batchSize) {
const batch = documentIds.slice(i, i + batchSize);
for (const docId of batch) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: docId,
permission: 'view'
};
const operation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT',
collectionPath: 'documents',
documentId: docId
};
try {
const doc = await executeAuthorizedFirestoreOperation(authorization, operation);
documents.push({ id: docId, ...doc });
} catch (error) {
console.error(Error fetching document ${docId}:, error);
// Continue with other documents
}
}
}
return documents;
}
// Share a document with another user
async shareDocument(documentId, userId, permission) {
// Map permission to relation
const relationMap = {
'view': 'viewer',
'edit': 'editor'
};
const relation = relationMap[permission];
if (!relation) {
throw new Error(Invalid permission: ${permission});
}
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: relation,
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Revoke access to a document
async revokeAccess(documentId, userId, permission) {
// Map permission to relation
const relationMap = {
'view': 'viewer',
'edit': 'editor'
};
const relation = relationMap[permission];
if (!relation) {
throw new Error(Invalid permission: ${permission});
}
const payload: Types.ManageRelationshipPayload = {
operation: 'DELETE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: relation,
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
}
`
$3
`typescript
import {
manageSpiceDBRelationship,
Types
} from 'auth-cloud-sdk';
class TeamAccessManager {
// Add a user to a team
async addUserToTeam(userId, teamId) {
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'team',
objectId: teamId
},
relation: 'member',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Grant a team access to a resource
async grantTeamAccess(teamId, resourceType, resourceId, permission) {
// Map permission to relation
const relationMap = {
'view': 'viewer',
'edit': 'editor'
};
const relation = relationMap[permission];
if (!relation) {
throw new Error(Invalid permission: ${permission});
}
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: resourceType,
objectId: resourceId
},
relation: relation,
subject: {
object: {
objectType: 'team',
objectId: teamId
},
optionalRelation: 'member'
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Remove a user from a team
async removeUserFromTeam(userId, teamId) {
const payload: Types.ManageRelationshipPayload = {
operation: 'DELETE',
relationships: [
{
resource: {
objectType: 'team',
objectId: teamId
},
relation: 'member',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
}
`
Error Handling
The Auth Cloud SDK provides consistent error handling across all functions. Here are common errors and how to handle them:
$3
These occur when there's no authenticated user or when token retrieval fails.
`typescript
import { executeAuthorizedFirestoreOperation } from 'auth-cloud-sdk';
try {
const result = await executeAuthorizedFirestoreOperation(/ ... /);
// Process result
} catch (error) {
if (error.message.includes('No authenticated user found')) {
// Handle unauthenticated user
console.error('User is not signed in');
// Redirect to login page
} else if (error.message.includes('Failed to retrieve Firebase ID token')) {
// Handle token retrieval failure
console.error('Authentication token error:', error);
// Prompt user to sign in again
} else {
// Handle other errors
console.error('Operation failed:', error);
}
}
`
$3
These occur when a user attempts an operation they're not authorized for.
`typescript
try {
const result = await executeAuthorizedFirestoreOperation(/ ... /);
// Process result
} catch (error) {
if (error.message.includes('Permission denied') ||
error.message.includes('not authorized')) {
// Handle permission denied
console.error('User does not have permission for this operation');
// Show appropriate UI message
} else {
// Handle other errors
console.error('Operation failed:', error);
}
}
`
$3
These occur when the Cloud Function cannot be reached.
`typescript
try {
const result = await executeAuthorizedFirestoreOperation(/ ... /);
// Process result
} catch (error) {
if (error.message.includes('Failed to fetch') ||
error.message.includes('Network error')) {
// Handle network issues
console.error('Network error:', error);
// Show offline message or retry option
} else {
// Handle other errors
console.error('Operation failed:', error);
}
}
`
$3
These are errors returned by the Cloud Functions themselves.
`typescript
try {
const result = await executeAuthorizedFirestoreOperation(/ ... /);
// Process result
} catch (error) {
if (error.message.includes('Cloud Function Error')) {
// Extract status code if available
const statusMatch = error.message.match(/status (\d+)/);
const statusCode = statusMatch ? parseInt(statusMatch[1]) : null;
if (statusCode === 400) {
// Handle bad request
console.error('Invalid request:', error);
} else if (statusCode === 403) {
// Handle forbidden
console.error('Permission denied:', error);
} else if (statusCode === 404) {
// Handle not found
console.error('Resource not found:', error);
} else if (statusCode === 500) {
// Handle server error
console.error('Server error:', error);
} else {
// Handle other Cloud Function errors
console.error('Cloud Function error:', error);
}
} else {
// Handle other errors
console.error('Operation failed:', error);
}
}
`
Best Practices
$3
1. Always verify authentication state before making SDK calls:
`typescript
if (!firebase.auth().currentUser) {
// Redirect to login or show authentication required message
return;
}
`
2. Use the principle of least privilege when defining permissions:
`typescript
// Instead of granting editor access when viewer would suffice:
const relation = userNeedsToEdit ? 'editor' : 'viewer';
`
3. Validate inputs before passing them to SDK functions:
`typescript
function validateDocumentId(id) {
if (!id || typeof id !== 'string' || id.trim() === '') {
throw new Error('Invalid document ID');
}
return id;
}
`
4. Don't store sensitive information in client-accessible Firestore documents:
`typescript
// Bad practice
const userData = {
name: 'User',
password: 'hashed_password', // Never store this client-side!
apiKeys: ['secret_key_1', 'secret_key_2'] // Never store this client-side!
};
// Good practice
const userData = {
name: 'User',
settings: { theme: 'dark', notifications: true }
};
`
5. Implement proper error handling for all SDK calls:
`typescript
try {
await executeAuthorizedFirestoreOperation(/ ... /);
} catch (error) {
// Log the error
console.error('Operation failed:', error);
// Show appropriate user message
if (error.message.includes('Permission denied')) {
showUserMessage('You don\'t have permission to perform this action');
} else {
showUserMessage('An error occurred. Please try again later.');
}
// Report to monitoring system if needed
reportErrorToMonitoring(error);
}
`
$3
1. Batch related operations when possible:
`typescript
// Instead of multiple separate relationship creations:
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{ / relationship 1 / },
{ / relationship 2 / },
{ / relationship 3 / }
]
};
await manageSpiceDBRelationship(payload);
`
2. Cache authorization results when appropriate:
`typescript
// Cache the list of viewable documents for a short time
let viewableDocumentsCache = { timestamp: 0, data: [] };
async function getViewableDocuments() {
const now = Date.now();
const cacheLifetime = 60 * 1000; // 1 minute
if (now - viewableDocumentsCache.timestamp < cacheLifetime) {
return viewableDocumentsCache.data;
}
const result = await lookupAuthorizedResources('document', 'view');
viewableDocumentsCache = {
timestamp: now,
data: result.resourceIds
};
return result.resourceIds;
}
`
3. Use query constraints to limit data transfer:
`typescript
const operation: Types.FirestoreOperation = {
operationType: 'GET_COLLECTION',
collectionPath: 'documents',
queryConstraints: [
{ limit: 10 }, // Only get 10 documents
{ orderBy: 'updatedAt', direction: 'desc' } // Get most recent first
]
};
`
4. Implement pagination for large result sets:
`typescript
async function getDocumentPage(lastDocId, pageSize = 10) {
const authorization: Types.Authorization = {
resourceObjectType: 'collection',
resourceObjectId: 'documents',
permission: 'list'
};
const operation: Types.FirestoreOperation = {
operationType: 'GET_COLLECTION',
collectionPath: 'documents',
queryConstraints: [
{ orderBy: 'createdAt', direction: 'desc' },
{ limit: pageSize }
]
};
// Add startAfter constraint if we have a last document ID
if (lastDocId) {
// First get the last document
const lastDocOp: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT',
collectionPath: 'documents',
documentId: lastDocId
};
const lastDoc = await executeAuthorizedFirestoreOperation(authorization, lastDocOp);
// Then add startAfter to the query constraints
operation.queryConstraints.push({
startAfter: lastDoc.createdAt
});
}
return executeAuthorizedFirestoreOperation(authorization, operation);
}
`
$3
1. Implement a service layer to abstract SDK calls:
`typescript
// documents.service.ts
import { executeAuthorizedFirestoreOperation, Types } from 'auth-cloud-sdk';
export class DocumentService {
async getDocument(id) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: id,
permission: 'read'
};
const operation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT',
collectionPath: 'documents',
documentId: id
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Other document-related methods...
}
`
2. Use dependency injection for testability:
`typescript
// In a React component with dependency injection
function DocumentViewer({ documentId, documentService }) {
const [document, setDocument] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadDocument() {
try {
const doc = await documentService.getDocument(documentId);
setDocument(doc);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
loadDocument();
}, [documentId, documentService]);
// Render component...
}
`
3. Implement retry logic for transient errors:
`typescript
async function executeWithRetry(fn, maxRetries = 3, delay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Only retry for certain errors
if (!isRetryableError(error) || attempt === maxRetries) {
throw error;
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
throw lastError;
}
function isRetryableError(error) {
// Determine if this error should be retried
return error.message.includes('network error') ||
error.message.includes('timeout') ||
error.message.includes('ECONNRESET');
}
// Usage
const result = await executeWithRetry(() =>
executeAuthorizedFirestoreOperation(authorization, operation)
);
`
Troubleshooting
$3
#### "Cloud Function endpoints not configured"
Problem: You're seeing an error about endpoints not being configured.
Solution: Ensure you've called setCloudFunctionEndpoints before using any SDK functions:
`typescript
import { setCloudFunctionEndpoints } from 'auth-cloud-sdk';
// Call this early in your application initialization
setCloudFunctionEndpoints({
executeAuthorizedFirestoreOperation: 'https://your-region-your-project.cloudfunctions.net/executeAuthorizedFirestoreOperation',
manageSpiceDBRelationship: 'https://your-region-your-project.cloudfunctions.net/manageSpiceDBRelationship',
writeSpiceDBSchema: 'https://your-region-your-project.cloudfunctions.net/writeSpiceDBSchema',
lookupAuthorizedResources: 'https://your-region-your-project.cloudfunctions.net/lookupAuthorizedResources'
});
`
#### "No authenticated user found"
Problem: You're trying to use SDK functions without a signed-in user.
Solution: Ensure the user is authenticated before making SDK calls:
`typescript
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
// Sign in the user first
const auth = getAuth();
await signInWithEmailAndPassword(auth, email, password);
// Now you can use SDK functions
const result = await executeAuthorizedFirestoreOperation(/ ... /);
`
#### "Permission denied" or "not authorized"
Problem: The user doesn't have the required permissions for the operation.
Solution: Check and update the user's permissions:
`typescript
import { manageSpiceDBRelationship, Types } from 'auth-cloud-sdk';
// Grant the necessary permission
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: 'viewer', // or 'editor', 'owner', etc.
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
await manageSpiceDBRelationship(payload);
`
#### "Failed to fetch" or Network Errors
Problem: The SDK can't reach the Cloud Functions.
Solution: Check your network connection and Cloud Function URLs:
1. Verify the Cloud Function URLs are correct
2. Ensure the Cloud Functions are deployed and running
3. Check for CORS issues if calling from a browser
4. Verify the Cloud Functions are in the same region as your application for best performance
#### "Invalid request" (400 errors)
Problem: The request to the Cloud Function is malformed.
Solution: Check your parameters and ensure they match the expected format:
`typescript
// Correct format for authorization
const authorization: Types.Authorization = {
resourceObjectType: 'document', // Required
resourceObjectId: documentId, // Required
permission: 'read' // Required
};
// Correct format for Firestore operation
const operation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT', // Required
collectionPath: 'documents', // Required
documentId: documentId // Required for GET_DOCUMENT
};
`
$3
1. Enable verbose logging:
`typescript
// Add this early in your application
const DEBUG = true;
function debugLog(...args) {
if (DEBUG) {
console.log('[Auth Cloud SDK]', ...args);
}
}
// Use throughout your code
debugLog('Calling executeAuthorizedFirestoreOperation with:', authorization, operation);
`
2. Check Firebase Authentication state:
`typescript
import { getAuth, onAuthStateChanged } from 'firebase/auth';
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
console.log('User is signed in:', user.uid);
user.getIdTokenResult(true).then(idTokenResult => {
console.log('User claims:', idTokenResult.claims);
});
} else {
console.log('No user is signed in');
}
});
`
3. Verify Cloud Function endpoints:
`typescript
import { getCloudFunctionEndpoints } from 'auth-cloud-sdk';
try {
const endpoints = getCloudFunctionEndpoints();
console.log('Configured endpoints:', endpoints);
} catch (error) {
console.error('Endpoints not configured:', error);
}
`
4. Test Cloud Functions directly:
`typescript
// Test a Cloud Function directly with fetch
async function testCloudFunction() {
const auth = getAuth();
const user = auth.currentUser;
if (!user) {
console.error('No user signed in');
return;
}
const idToken = await user.getIdToken();
const endpoint = 'https://your-region-your-project.cloudfunctions.net/lookupAuthorizedResources';
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
firebaseToken: idToken,
resourceObjectType: 'document',
permission: 'view'
})
});
if (!response.ok) {
const errorText = await response.text();
console.error(Error ${response.status}:, errorText);
} else {
const result = await response.json();
console.log('Success:', result);
}
} catch (error) {
console.error('Fetch error:', error);
}
}
`
FAQ
$3
Q: Do I need to initialize Firebase before using the SDK?
A: Yes, you must initialize Firebase in your application before using the SDK. The SDK relies on the Firebase Auth instance to get the current user and their ID token.
Q: Can I use the SDK with any Firebase project?
A: Yes, but you need to deploy the corresponding Cloud Functions to your Firebase project. The SDK is designed to work with specific Cloud Functions that handle authorization and Firestore operations.
Q: Does the SDK work with Firebase Emulator Suite?
A: Yes, you can use the SDK with Firebase Emulator Suite for local development. Just configure the SDK with the local URLs of your emulated Cloud Functions.
`typescript
setCloudFunctionEndpoints({
executeAuthorizedFirestoreOperation: 'http://localhost:5001/your-project/us-central1/executeAuthorizedFirestoreOperation',
manageSpiceDBRelationship: 'http://localhost:5001/your-project/us-central1/manageSpiceDBRelationship',
writeSpiceDBSchema: 'http://localhost:5001/your-project/us-central1/writeSpiceDBSchema',
lookupAuthorizedResources: 'http://localhost:5001/your-project/us-central1/lookupAuthorizedResources'
});
`
$3
Q: How does the SDK handle authentication?
A: The SDK uses Firebase Authentication. It retrieves the current user's ID token and sends it to the Cloud Functions for verification. The Cloud Functions then use this token to authenticate the user and perform the requested operations.
Q: What happens if the user's token expires?
A: The SDK automatically requests a fresh token when needed. If the token has expired, the getFirebaseIdToken function will force a refresh.
Q: Can I use custom authentication with the SDK?
A: The SDK is designed to work with Firebase Authentication. If you're using custom authentication, you'll need to integrate it with Firebase Auth using Custom Auth Providers.
$3
Q: What is SpiceDB and why is it used?
A: SpiceDB (formerly Authzed) is an open-source, fine-grained permissions system. It's used to manage authorization relationships and check permissions. The SDK uses SpiceDB through Cloud Functions to provide robust authorization capabilities.
Q: How do I define permissions in SpiceDB?
A: Permissions in SpiceDB are defined through a schema. You can write and update this schema using the writeSpiceDBSchema function. Here's an example schema:
`
definition user {}
definition document {
relation viewer: user
relation editor: user
relation owner: user
permission view = viewer + editor + owner
permission edit = editor + owner
permission delete = owner
}
`
Q: Can I use role-based access control (RBAC) with the SDK?
A: Yes, you can implement RBAC using SpiceDB relationships. For example:
`typescript
// Define roles in your SpiceDB schema
const schema =
;
// Assign a user to a role
const assignRolePayload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'role',
objectId: 'admin'
},
relation: 'member',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
// Grant a role access to a document
const grantRoleAccessPayload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: 'editor',
subject: {
object: {
objectType: 'role',
objectId: 'admin'
}
}
}
]
};
`
$3
Q: Does the SDK replace the Firestore SDK?
A: No, the SDK complements the Firestore SDK by adding authorization checks. It's designed for operations that require permission checks, but you can still use the regular Firestore SDK for operations that don't need authorization.
Q: Can I use the SDK with Firestore Security Rules?
A: Yes, but they serve different purposes. Firestore Security Rules provide client-side security, while the SDK provides server-side security through Cloud Functions. You can use both together for enhanced security.
Q: How does the SDK handle Firestore transactions?
A: The SDK doesn't directly support Firestore transactions. For operations that require transactions, you should create a dedicated Cloud Function that performs the transaction and call it through the SDK.
$3
Q: How do I update the SDK to a new version?
A: Update the SDK using npm or yarn:
`bash
npm update auth-cloud-sdk
or
yarn upgrade auth-cloud-sdk
``