# Stoker Platform Stoker is a framework for building realtime, offline-ready internal tools and SaaS applications on Google Cloud Platform and Firebase. It provides a declarative schema-based approach to define collections (data models), access control policies, and admin UI configurations, then automatically generates Firestore security rules, indexes, Cloud Functions, and a fully functional admin interface. The platform handles complex features like multi-tenancy, role-based access control, two-way relations, denormalized data synchronization, and offline persistence out of the box. The core workflow involves defining your app's global configuration in `src/main.ts` and collection schemas in `src/collections/*.ts`, then using the Stoker CLI to deploy infrastructure to Google Cloud. Stoker provides both Web and Node SDKs for programmatic data access, along with a callable Cloud Functions API for external integrations. The generated admin UI includes list views, board/kanban views, calendar views, map views, file management, AI chat (RAG), and comprehensive CRUD operations with real-time updates. ## CLI Installation and Project Setup The Stoker CLI is the primary tool for bootstrapping, configuring, and deploying Stoker applications. It handles project initialization, Google Cloud project creation, tenant management, schema deployment, and various data operations. ```bash # Install prerequisites npm i -g firebase-tools firebase login npm i -g genkit-cli # Install Stoker CLI npm i -g @stoker-platform/cli # Initialize a new Stoker project mkdir my-app && cd my-app stoker init && git init && npm i && npm --prefix functions i # Add a development project (takes ~20 minutes) stoker add-project -n my-dev-project --development # Set the active project export GCP_PROJECT=my-dev-project && stoker set-project # Prepare emulator data and start local development stoker emulator-data npm run start # Add a production project stoker add-project -n my-prod-project # Deploy changes to the active project stoker deploy # Add a tenant to a project stoker add-tenant # Add a custom domain stoker custom-domain --domain example.com # Seed test data stoker seed-data --collection Clients --count 100 # Export Firestore data to BigQuery stoker bigquery --collection Clients ``` ## Global Config File (src/main.ts) The global config file defines project-wide settings including user roles, authentication, admin UI configuration, and global hooks. This is the central configuration that governs how your entire application behaves. ```typescript // src/main.ts import type { GenerateGlobalConfig } from "@stoker-platform/types"; import { Mail, Users } from "lucide-react"; export const GenerateGlobalConfig: GenerateGlobalConfig = (sdk, utils, context) => ({ // Required: Define user roles for your application roles: ["Admin", "Manager", "Staff", "Client"], // Required: Application name (used in page titles) appName: "My Business App", // Required: IANA timezone for the application timezone: "America/New_York", // Authentication configuration auth: { enableMultiFactorAuth: ["Admin", "Manager"], authPersistenceType: "LOCAL", offlinePersistenceType: (user, claims) => { return claims.role === "Admin" ? "ALL" : "WRITE"; }, signOutOnPermissionsChange: false, clearPersistenceOnSignOut: true, tabManager: "MULTI", garbageCollectionStrategy: "LRU", maxCacheSize: -1, }, // Admin UI configuration admin: { access: ["Admin", "Manager", "Staff"], dateFormat: "yyyy-MM-dd", background: { light: { color: "#f8fafc" }, dark: { color: "#0f172a" }, }, homePage: { Admin: "Users", Manager: "Projects", Staff: "Tasks", }, menu: { groups: [ { title: "Messages", position: 1, collections: ["Inbox", "Outbox"], roles: ["Admin", "Manager"], }, { title: "Operations", position: 2, collections: ["Projects", "Tasks", "Clients"], }, ], }, dashboard: [ { kind: "metric", collection: "Tasks", type: "count", title: "Open Tasks", roles: ["Admin", "Manager"], textSize: "text-3xl", }, { kind: "chart", collection: "Tasks", type: "area", dateField: "Created_At", defaultRange: "30d", title: "Tasks Over Time", }, { kind: "reminder", collection: "Tasks", columns: ["Name", "Due_Date", "Assigned_To"], title: "Overdue Tasks", constraints: [["Status", "==", "In Progress"]], sort: { field: "Due_Date", direction: "asc" }, }, ], }, // Firebase configuration firebase: { enableEmulators: () => import.meta.env.DEV, enableAnalytics: () => import.meta.env.PROD, serverTimestampOptions: "estimate", logLevel: { dev: "warn", prod: "error" }, writeLogTTL: 90, permissionsIndexExemption: true, }, // Preload cache configuration preload: { async: ["Tasks", "Projects"], sync: ["Users", "Settings"], }, // Mail configuration mail: { emailVerification: (verificationLink, appName) => ({ subject: `Verify your ${appName} account`, html: `

Click here to verify your email.

`, }), }, // Global hooks custom: { postLogin: async (user) => { console.log(`User ${user?.email} logged in`); }, postWriteError: async (operation, data, docId, context, error) => { console.error(`Write error on ${operation}:`, error); }, onConnectionStatusChange: (status, first) => { if (!first && status === "Offline") { console.log("Connection lost"); } }, }, }); export default GenerateGlobalConfig; ``` ## Collection Config File (src/collections/*.ts) Collection config files define the schema, fields, access controls, and UI configuration for each data collection. Each collection maps to a Firestore collection and a page in the admin UI. ```typescript // src/collections/Tasks.ts import type { GenerateSchema } from "@stoker-platform/types"; import { CheckSquare, Calendar, User } from "lucide-react"; export const GenerateSchema: GenerateSchema = (sdk, utils, context) => ({ // Collection labels labels: { collection: "Tasks", record: "Task" }, recordTitleField: "Name", // Enable write logging for audit trail enableWriteLog: true, // Full text search on these fields fullTextSearch: ["Name", "Description"], // Show related records on the record page relationLists: [ { collection: "Subtasks", field: "Task", roles: ["Admin", "Manager"] }, ], // Soft delete configuration softDelete: { archivedField: "Archived", timestampField: "Archived_At", retentionPeriod: 30, }, // Access configuration access: { operations: { read: ["Admin", "Manager", "Staff"], create: ["Admin", "Manager"], update: ["Admin", "Manager", "Staff"], delete: ["Admin"], }, attributeRestrictions: [ { type: "Record_Owner", roles: [{ role: "Staff", assignable: true }], operations: ["Read", "Update"], }, { type: "Record_User", roles: [{ role: "Staff" }], collectionField: "Assigned_To", operations: ["Read", "Update"], }, { type: "Record_Property", roles: [ { role: "Staff", values: ["Not Started", "In Progress"] }, { role: "Manager", values: ["Not Started", "In Progress", "Completed"] }, ], propertyField: "Status", }, ], entityRestrictions: { restrictions: [ { type: "Parent", roles: [{ role: "Staff" }], collectionField: "Project", }, ], }, }, // Preload cache for offline access preloadCache: { roles: ["Admin", "Manager", "Staff"], range: { fields: ["Due_Date", "Created_At"], start: "Month", startOffsetDays: -7, selector: ["range", "week", "month"], }, }, // Field definitions fields: [ { name: "Name", type: "String", required: true, unique: true, maxlength: 200, admin: { label: "Task Name", icon: { component: CheckSquare }, column: 1, }, }, { name: "Description", type: "String", admin: { textarea: true, column: 2, }, }, { name: "Status", type: "String", required: true, values: ["Not Started", "In Progress", "Review", "Completed"], admin: { badge: (record) => { const colors: Record = { "Not Started": "bg-gray-100 text-gray-800", "In Progress": "bg-blue-100 text-blue-800", "Review": "bg-yellow-100 text-yellow-800", "Completed": "bg-green-100 text-green-800", }; return colors[record?.Status] || "bg-gray-100"; }, column: 3, }, custom: { initialValue: () => "Not Started", }, }, { name: "Priority", type: "Number", values: [1, 2, 3, 4, 5], admin: { slider: true, column: 4, }, }, { name: "Due_Date", type: "Timestamp", required: true, admin: { icon: { component: Calendar }, time: true, column: 5, }, }, { name: "Estimated_Hours", type: "Number", decimal: 2, min: 0, max: 1000, }, { name: "Assigned_To", type: "ManyToOne", collection: "Users", includeFields: ["Name", "Email"], titleField: "Name", admin: { icon: { component: User }, }, }, { name: "Project", type: "ManyToOne", collection: "Projects", includeFields: ["Name", "Client"], titleField: "Name", dependencyFields: [ { field: "Name", roles: ["Staff"] }, ], }, { name: "Tags", type: "Array", values: ["Urgent", "Bug", "Feature", "Documentation"], admin: { tags: ["bg-red-100", "bg-orange-100", "bg-blue-100", "bg-purple-100"], }, }, { name: "Completed_Hours", type: "Computed", formula: (record, retrieverData) => { // Access preloaded data from retriever const timeEntries = retrieverData?.timeEntries || []; return timeEntries .filter((e: any) => e.Task_ID === record.id) .reduce((sum: number, e: any) => sum + (e.Hours || 0), 0); }, }, { name: "Archived", type: "Boolean", access: ["Admin"], }, { name: "Archived_At", type: "Timestamp", access: ["Admin"], }, ], // Admin UI configuration admin: { navbarPosition: 2, titles: { collection: "Tasks", record: "Task" }, icon: CheckSquare, itemsPerPage: 25, statusField: { field: "Status", active: ["Not Started", "In Progress", "Review"], archived: ["Completed"], }, defaultSort: { field: "Due_Date", direction: "asc" }, duplicate: true, breadcrumbs: ["Project"], // Board/Kanban view cards: { title: "Board", statusField: "Status", headerField: "Name", maxHeaderLines: 2, sections: [ { fields: ["Assigned_To", "Due_Date"], blocks: true, }, { title: "Priority", fields: ["Priority"], large: true, }, ], footerField: "Project", }, // Calendar view calendar: { title: "Calendar", startField: "Due_Date", eventTitle: (record) => `${record.Name} (${record.Status})`, color: (record) => record.Priority > 3 ? "#ef4444" : "#3b82f6", unscheduled: { title: "Unscheduled" }, }, // Filters filters: [ { type: "select", field: "Status", style: "buttons" }, { type: "select", field: "Priority" }, { type: "relation", field: "Assigned_To" }, { type: "range", field: "Due_Date", selector: ["range", "week", "month"] }, ], // Metrics at top of list metrics: [ { type: "count", title: "Total Tasks" }, { type: "sum", field: "Estimated_Hours", title: "Est. Hours", decimal: 1 }, { type: "area", dateField: "Created_At", metricField1: "Estimated_Hours", defaultRange: "30d", }, ], // Custom form buttons formButtons: [ { title: "Mark Complete", variant: "default", action: async (operation, formValues, originalRecord) => { const { updateRecord } = await import("@stoker-platform/web-client"); await updateRecord(["Tasks"], formValues.id, { Status: "Completed" }); }, condition: (operation, record) => operation === "update" && record?.Status !== "Completed", }, ], // Row highlighting rowHighlight: [ { condition: (record) => record.Priority === 5, className: "bg-red-50 dark:bg-red-950", roles: ["Admin", "Manager"], }, ], // Data retriever for computed fields retriever: async () => { const { getSome } = await import("@stoker-platform/web-client"); const { docs } = await getSome(["TimeEntries"]); return { timeEntries: docs }; }, }, // Collection hooks custom: { preValidate: async (operation, record, context) => { if (record.Due_Date && record.Due_Date < new Date()) { return { valid: false, message: "Due date cannot be in the past" }; } return { valid: true }; }, preWrite: async (operation, data, docId, context, batch, originalRecord) => { if (operation === "update" && data.Status === "Completed") { data.Completed_At = new Date(); } return true; }, postWrite: async (operation, data, docId, context) => { if (sdk === "node" && operation === "create") { const { sendMail } = await import("@stoker-platform/node-client"); await sendMail( data.Assigned_To_Email, `New Task Assigned: ${data.Name}`, `You have been assigned a new task: ${data.Name}` ); } }, }, // AI Chat configuration ai: { embedding: true, chat: { name: "Task Assistant", defaultQueryLimit: 10, roles: ["Admin", "Manager"], }, }, }); export default GenerateSchema; ``` ## Web SDK Usage The Web SDK provides client-side functions for authentication, CRUD operations, real-time subscriptions, and utility functions. It powers the admin UI and can be used in custom applications. ```typescript import { initializeStoker, authenticateStoker, onStokerReady, signOut, addRecord, updateRecord, deleteRecord, getOne, getSome, subscribeOne, subscribeMany, preloadCollection, sendMail, sendMessage, convertTimestampToTimezone, displayDate, waitForPendingWrites, getFiles, multiFactorEnroll, } from "@stoker-platform/web-client"; // Initialize the Stoker app const config = await import("./config/main"); const collectionFiles = import.meta.glob("./config/collections/*", { eager: true }); const isLoggedIn = await initializeStoker(config, collectionFiles, import.meta.env); // Authenticate user try { await authenticateStoker( "user@example.com", "password123", // Optional MFA handler async () => { return prompt("Enter your authenticator code:") || ""; } ); } catch (error) { console.error("Authentication failed:", error); } // Listen for successful authentication const removeListener = onStokerReady(() => { console.log("User authenticated and Stoker ready!"); }); // Create a new record const newTask = await addRecord( ["Tasks"], { Name: "Complete documentation", Description: "Write API documentation for the new features", Status: "Not Started", Priority: 3, Due_Date: new Date("2024-12-31"), Tags: ["Documentation"], }, undefined, // user credentials (for auth collections) undefined, // options undefined, // custom ID () => console.log("Validation passed") ); console.log("Created task:", newTask.id); // Create a user record with authentication const newUser = await addRecord( ["Users"], { Name: "John Smith", Email: "john@example.com", Role: "Staff", }, { password: "SecurePass123!", passwordConfirm: "SecurePass123!", permissions: { Role: "Staff", Enabled: true, collections: { Tasks: { operations: ["Read", "Update"], recordOwner: { active: true }, }, }, }, } ); // Update a record (partial update) const updatedTask = await updateRecord( ["Tasks"], "taskId123", { Status: "In Progress", Priority: 4, } ); // Delete a field using Firestore sentinel import { deleteField } from "firebase/firestore"; await updateRecord(["Tasks"], "taskId123", { Description: deleteField(), }); // Delete a record const deletedTask = await deleteRecord(["Tasks"], "taskId123"); // Get a single record with relations const task = await getOne( ["Tasks"], "taskId123", { relations: { depth: 2, fields: ["Project", "Assigned_To"] }, subcollections: { collections: ["Subtasks"], depth: 1, limit: { number: 10, orderByField: "Created_At", orderByDirection: "desc" }, }, } ); console.log("Task project:", task.Project_Name); console.log("Subtasks:", task._subcollections?.Subtasks); // Get multiple records with constraints and pagination import { where, orderBy } from "firebase/firestore"; const { docs, cursor, pages } = await getSome( ["Tasks"], [ where("Status", "==", "In Progress"), where("Priority", ">=", 3), ], { relations: { depth: 1 }, pagination: { number: 25, orderByField: "Due_Date", orderByDirection: "asc", }, } ); console.log(`Found ${docs.length} tasks across ${pages} pages`); // Get next page const nextPage = await getSome( ["Tasks"], [where("Status", "==", "In Progress")], { pagination: { number: 25, orderByField: "Due_Date", orderByDirection: "asc", startAfter: cursor, }, } ); // Subscribe to a single record const unsubscribe = await subscribeOne( ["Tasks"], "taskId123", (task) => { if (task) { console.log("Task updated:", task.Name, task.Status); } else { console.log("Task deleted"); } }, (error) => console.error("Subscription error:", error), { relations: true, only: "default", // "cache" or "default" } ); // Subscribe to a collection const { unsubscribe: unsubMany, pages: totalPages, count } = await subscribeMany( ["Tasks"], [where("Assigned_To", "==", "userId123")], (docs, cursor, metadata) => { console.log(`Received ${docs.length} tasks`); console.log("From cache:", metadata?.fromCache); }, (error) => console.error("Error:", error), { relations: { fields: ["Project"] }, pagination: { number: 50, orderByField: "Due_Date", orderByDirection: "asc", }, } ); // Clean up subscriptions unsubscribe(); unsubMany(); // Wait for all pending writes to complete await waitForPendingWrites(); console.log("All writes persisted to server"); // Preload/reload collection cache await preloadCollection("Tasks", [["Status", "!=", "Completed"]]); // Get files for a record const files = await getFiles("Tasks", task); files.forEach((file) => { console.log(`File: ${file.name}, Size: ${file.size}`); }); // Send email await sendMail( ["recipient@example.com"], "Task Update", "Your task has been updated.", "

Task Update

Your task has been updated.

", ["cc@example.com"], ["bcc@example.com"], "reply@example.com" ); // Send SMS await sendMessage("+15551234567", "Your task is overdue!"); // Date utilities const luxonDate = convertTimestampToTimezone(task.Due_Date); console.log("Due date in app timezone:", luxonDate.toFormat("yyyy-MM-dd HH:mm")); console.log("Formatted date:", displayDate(task.Due_Date)); // Enroll in MFA import { getCurrentUser } from "@stoker-platform/web-client"; const user = getCurrentUser(); await multiFactorEnroll( user, async (secret, totpUri) => { // Display QR code using totpUri, then get code from user return prompt("Enter the code from your authenticator app:") || ""; } ); // Sign out await signOut(); ``` ## Node SDK Usage The Node SDK provides server-side functions for data operations, typically used in Cloud Functions, CLI scripts, and backend services. It includes additional features like transaction support and user impersonation. ```typescript import { initializeStoker, fetchCurrentSchema, addRecord, updateRecord, deleteRecord, getOne, getSome, sendMail, sendMessage, convertTimestampToTimezone, displayDate, } from "@stoker-platform/node-client"; import { join } from "path"; import { getFirestore } from "firebase-admin/firestore"; // Initialize Stoker in Node environment const utils = await initializeStoker( "production", join(process.cwd(), "config", "main.js"), join(process.cwd(), "config", "collections"), true // Running in GCP environment ); // Fetch current schema const schema = await fetchCurrentSchema(true); // include computed fields console.log("Collections:", Object.keys(schema.collections)); // Create a record const newTask = await addRecord( ["Tasks"], { Name: "Backend task", Status: "Not Started", Priority: 2, Due_Date: new Date("2024-12-15"), }, undefined, // user credentials "adminUserId", // impersonate user { noTwoWay: false, // write two-way relations }, { source: "api" }, // context passed to hooks "custom-task-id" // optional custom ID ); // Create user with authentication const newUser = await addRecord( ["Users"], { Name: "Jane Doe", Email: "jane@example.com", }, { password: "SecurePassword123!", permissions: { Role: "Manager", Enabled: true, collections: { Tasks: { operations: ["Read", "Create", "Update", "Delete"] }, Projects: { operations: ["Read", "Create", "Update"] }, }, }, } ); // Update a record with user impersonation const updatedTask = await updateRecord( ["Tasks"], "taskId123", { Status: "Completed", Completed_At: new Date(), }, undefined, // user credentials update "managerUserId", // impersonate this user { noTwoWay: false }, { source: "scheduled-job" } ); // Delete a record (with force for soft-delete collections) const deletedTask = await deleteRecord( ["Tasks"], "taskId123", "adminUserId", { force: true }, // permanently delete even if soft-delete enabled { reason: "cleanup" } ); // Get a single record with relations and subcollections const task = await getOne( ["Tasks"], "taskId123", { user: "userId123", relations: { depth: 2, fields: ["Project", "Assigned_To"], }, subcollections: { collections: ["Subtasks", "Comments"], depth: 1, constraints: [["Status", "!=", "Deleted"]], limit: { number: 20, orderByField: "Created_At", orderByDirection: "desc", }, }, noComputedFields: false, noEmbeddingFields: true, } ); // Get multiple records with pagination const { docs, cursor, pages } = await getSome( ["Tasks"], [ ["Status", "in", ["Not Started", "In Progress"]], ["Priority", ">=", 3], ], { user: "managerId", relations: { depth: 1 }, pagination: { number: 100, orderByField: "Created_At", orderByDirection: "desc", }, transactional: false, } ); // Batch operations with transactions const db = getFirestore(); await db.runTransaction(async (transaction) => { // Fetch schema once for all operations const schema = await fetchCurrentSchema(); // Create multiple records in a single transaction for (const taskData of tasksToCreate) { await addRecord( ["Tasks"], taskData, undefined, "adminUserId", { providedTransaction: transaction, providedSchema: schema, } ); } // Update records in same transaction for (const { id, updates } of tasksToUpdate) { const originalRecord = await getOne(["Tasks"], id, { providedTransaction: transaction }); await updateRecord( ["Tasks"], id, updates, undefined, "adminUserId", { providedTransaction: transaction, providedSchema: schema, }, undefined, originalRecord ); } }); // Send email with attachments await sendMail( ["user@example.com", "manager@example.com"], "Daily Task Report", "Please find attached the daily task report.", "

Daily Task Report

See attached PDF.

", undefined, // cc undefined, // bcc "noreply@example.com", [ { filename: "report.pdf", content: pdfBuffer, contentType: "application/pdf", }, ], "reports@myapp.com" // custom from address ); // Send SMS await sendMessage("+15551234567", "Your weekly summary is ready!"); // Date utilities const dueDate = convertTimestampToTimezone(task.Due_Date); console.log("Due:", dueDate.toFormat("MMMM d, yyyy")); console.log("Display:", displayDate(task.Due_Date)); ``` ## Callable API (Cloud Functions) Stoker provides a callable Cloud Functions API for external integrations. The API uses Firebase callable functions for secure, scalable access. ```typescript // Client-side: Using the Stoker API via Firebase callable functions import { initializeApp } from "firebase/app"; import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; import { getFunctions, httpsCallable } from "firebase/functions"; // Initialize Firebase const app = initializeApp({ apiKey: "your-api-key", authDomain: "your-project.firebaseapp.com", projectId: "your-project", }); // Authenticate const auth = getAuth(app); await signInWithEmailAndPassword(auth, "user@example.com", "password"); // Get functions instance const functions = getFunctions(app); // Read API - Get single record const readApi = httpsCallable(functions, "stoker-readapi"); const singleResult = await readApi({ path: ["Tasks"], id: "taskId123", options: { relations: { depth: 1 }, }, }); console.log("Task:", singleResult.data.result); // Read API - Get multiple records with constraints const multiResult = await readApi({ path: ["Tasks"], constraints: [ ["Status", "==", "In Progress"], ["Priority", ">=", 3], ], options: { pagination: { number: 50 }, }, }); console.log("Tasks:", multiResult.data.result); // Read API - Get subcollection records const subtasksResult = await readApi({ path: ["Tasks", "taskId123", "Subtasks"], constraints: [["Completed", "==", false]], }); // Write API - Create record const writeApi = httpsCallable(functions, "stoker-writeapi"); const createResult = await writeApi({ operation: "create", path: ["Tasks"], record: { Name: "API Created Task", Status: "Not Started", Priority: 2, Due_Date: new Date().toISOString(), }, }); console.log("Created:", createResult.data.result); // Write API - Update record const updateResult = await writeApi({ operation: "update", path: ["Tasks"], id: "taskId123", record: { Status: "Completed", Completed_At: new Date().toISOString(), }, }); // Write API - Delete record const deleteResult = await writeApi({ operation: "delete", path: ["Tasks"], id: "taskId123", }); // Write API - Create user with credentials const userResult = await writeApi({ operation: "create", path: ["Users"], record: { Name: "New User", Email: "newuser@example.com", }, userData: { password: "SecurePass123!", permissions: { Role: "Staff", Enabled: true, collections: { Tasks: { operations: ["Read", "Update"] }, }, }, }, }); // Search API const searchApi = httpsCallable(functions, "stoker-searchapi"); const searchResult = await searchApi({ collection: "Tasks", query: "documentation", hitsPerPage: 20, constraints: [["Status", "!=", "Completed"]], }); console.log("Search results:", searchResult.data); // Array of record IDs ``` ## Environment Configuration (.env/.env) The environment file configures backend infrastructure including Google Cloud regions, Firebase services, and third-party integrations. ```bash # .env/.env - Default environment configuration # Contact Information (Required) ADMIN_EMAIL=admin@example.com ADMIN_SMS=+15551234567 # Google Cloud Config (Required) GCP_BILLING_ACCOUNT=012345-6789AB-CDEF01 GCP_ORGANIZATION=123456789 GCP_FOLDER=987654321 FB_GOOGLE_ANALYTICS_ACCOUNT_ID=12345678 # Firestore Config FB_FIRESTORE_REGION=us-central1 FB_FIRESTORE_ENABLE_PITR=true FB_FIRESTORE_BACKUP_RECURRENCE=daily FB_FIRESTORE_BACKUP_RETENTION=30d # Realtime Database Config FB_DATABASE_REGION=us-central1 # Cloud Storage Config FB_STORAGE_REGION=us-central1 FB_STORAGE_ENABLE_VERSIONING=true FB_STORAGE_SOFT_DELETE_DURATION=30d # Firebase Auth Config FB_AUTH_PASSWORD_POLICY={"enforcementState":"ENFORCE","forceUpgradeOnSignin":true,"constraints":{"requireUppercase":true,"requireLowercase":true,"requireNonAlphanumeric":true,"requireNumeric":true,"minLength":12,"maxLength":128,"containsNonAlphanumericCharacter":true,"containsUppercaseCharacter":true,"containsLowercaseCharacter":true,"containsNumericCharacter":true}} FB_AUTH_PASSWORD_POLICY_UPGRADE=true # Cloud Functions Config FB_FUNCTIONS_REGION=us-central1 FB_FUNCTIONS_V1_REGION=us-central1 FB_FUNCTIONS_MEMORY=512MiB FB_FUNCTIONS_TIMEOUT=60s FB_FUNCTIONS_MAX_INSTANCES=100 FB_FUNCTIONS_MIN_INSTANCES=0 FB_FUNCTIONS_CPU=1 FB_FUNCTIONS_CONCURRENCY=80 FB_FUNCTIONS_CONSUME_APP_CHECK_TOKEN=false # Firebase Hosting Config FB_HOSTING_ENABLE_CLOUD_LOGGING=true FB_HOSTING_MAX_VERSIONS=10 # App Check Config (Recommended) FB_ENABLE_APP_CHECK=true FB_APP_CHECK_TOKEN_TTL=3600s # AI / Genkit Config FB_AI_REGION=us-central1 # Mail Config (Required) MAIL_REGION=us-central1 MAIL_SENDER=My App MAIL_SMTP_CONNECTION_URI=smtps://username@gmail.com@smtp.gmail.com:465 MAIL_SMTP_PASSWORD=xxxx-xxxx-xxxx-xxxx # SMS Config (Optional - Twilio) TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+15551234567 # Algolia Config (Optional - for large collection search) ALGOLIA_ID=XXXXXXXXXX ALGOLIA_ADMIN_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Sentry Config (Optional) SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx # Fullcalendar License (Required for calendar view) FULLCALENDAR_KEY=your-license-key # External Secrets (for custom Cloud Functions) EXTERNAL_SECRETS={"XERO_ID":"xxx","XERO_SECRET":"xxx","STRIPE_KEY":"sk_xxx"} ``` ## Application State Utilities Application state provides context-aware utilities for accessing app configuration, user information, and system state within config files and hooks. ```typescript // Using application state in config files import type { GenerateSchema } from "@stoker-platform/types"; export const GenerateSchema: GenerateSchema = (sdk, utils, context) => { // sdk: "web" | "node" - current environment // Web SDK utilities if (sdk === "web") { const tenant = utils.getTenant(); const env = utils.getEnv(); const timezone = utils.getTimezone(); const connectionStatus = utils.getConnectionStatus(); // "Online" | "Offline" const networkStatus = utils.getNetworkStatus(); const schema = utils.getSchema(true); // include computed fields const user = utils.getCurrentUser(); const userRole = user?.token?.claims?.role; const globalConfig = utils.getGlobalConfigModule(); const taskConfig = utils.getCollectionConfigModule("Tasks"); const versionInfo = utils.getVersionInfo(); const maintenanceInfo = utils.getMaintenanceInfo(); const permissions = utils.getCurrentUserPermissions(); const loadingState = utils.getLoadingState(); // Firebase instances const appCheck = utils.getAppCheck(); const firestoreWrite = utils.getFirestoreWrite(); // Context utilities (from Admin UI) context.setDialogContent({ title: "Confirm Action", description: "Are you sure?", buttons: [ { label: "Cancel", onClick: () => context.setDialogContent(null) }, { label: "Confirm", onClick: async () => { /* action */ } }, ], }); context.setGlobalLoading("+", "recordId"); context.createRecordForm(schema.collections.Tasks, ["Tasks"]); } // Node SDK utilities if (sdk === "node") { const mode = utils.getMode(); // "development" | "production" const tenant = utils.getTenant(); utils.setTenant("newTenantId"); const timezone = utils.getTimezone(); const globalConfig = utils.getGlobalConfigModule(); const taskCustomization = utils.getCustomizationFile("Tasks", schema); const versionInfo = utils.getVersionInfo(); const maintenanceInfo = utils.getMaintenanceInfo(); } return { labels: { collection: "Tasks", record: "Task" }, // ... rest of schema // Use utilities in field definitions fields: [ { name: "Status", type: "String", values: ["Active", "Completed"], admin: { // Conditionally show based on user role condition: { form: () => { if (sdk === "web") { const user = utils.getCurrentUser(); return user?.token?.claims?.role === "Admin"; } return true; }, }, }, }, { name: "Internal_Notes", type: "String", // Only accessible when online admin: { readOnly: () => { if (sdk === "web") { return utils.getConnectionStatus() === "Offline"; } return false; }, }, }, ], // Use utilities in hooks custom: { preWrite: async (operation, data, docId, ctx) => { if (sdk === "node") { const mode = utils.getMode(); if (mode === "development") { console.log("Dev mode write:", data); } } return true; }, }, }; }; ``` Stoker is designed for building enterprise-grade internal tools and SaaS applications that require offline capabilities, real-time updates, complex access control, and multi-tenancy. The declarative schema-based approach dramatically reduces boilerplate code while the generated admin UI provides immediate value for back-office operations. Common use cases include CRM systems, project management tools, inventory management, field service applications, and any workflow-heavy business application. Integration typically involves defining your data model as collection schemas, configuring role-based access control policies, customizing the admin UI views, and optionally extending functionality through hooks and custom Cloud Functions. The Web and Node SDKs enable building custom frontends and backend integrations while maintaining consistency with the core access control and validation rules. For external systems, the callable API provides secure programmatic access to all CRUD operations.