# 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: RecordYour 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.", "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