Introduction
🎉 API v2.0 & Flutter SDK v2.0 Released (May 2026)
Breaking Changes: Authentication now uses multi-tenant user isolation. Each app has its own user base.
• API: Registration requires app_uuid in request body
• API: Endpoints changed: /register, /login, /me, /logout (no /auth prefix)
• Flutter SDK: Firestack.initialize() now requires appId parameter
• Flutter SDK: signUp() simplified (no passwordConfirmation, name is optional)
• User model now includes project_id, app_uuid, custom_claims, and metadata
FireStack is a self-hosted, open-source Backend as a Service built on Laravel. It provides all the services mobile and web apps typically need — authentication, realtime database, file storage, real-time messaging, push notifications, remote config, and analytics — exposed as a clean REST API.
Base URL
All API requests require an X-API-Key header. Authenticated routes also require Authorization: Bearer <token>.
What's Included
Authentication Model
Every request uses a two-layer auth model:
| Layer | Header | Purpose |
|---|---|---|
| 1 — App Identity | X-API-Key | Identifies which app & project the request belongs to |
| 2 — User Identity | Authorization: Bearer | Identifies the authenticated user (Sanctum token) |
Response Format
{
"success": true,
"data": { ... },
"message": "Operation successful"
}
{
"success": false,
"message": "Validation failed",
"errors": { "email": ["The email field is required."] },
"error_code": "VALIDATION_ERROR",
"timestamp": "2026-04-18T12:00:00Z"
}
Installation
git clone https://github.com/your-org/firestack.git cd firestack
composer install npm install
cp .env.example .env php artisan key:generate
php artisan migrate --seed
npm run build
php artisan serve # Visit http://localhost:8000
Default Credentials
| User | Password | |
|---|---|---|
| Admin | admin@firestack.test | password |
| Test User | test@example.com | password |
Optional Services
php artisan reverb:start
php artisan queue:work --queue=default,notifications,webhooks
Authentication Setup
FireStack uses a two-layer authentication model. Every request requires an API Key that identifies your app. User-scoped endpoints additionally require a Bearer token obtained by registering or logging in a user.
Two-Layer Auth Model
# Layer 1 — always required X-API-Key: fsk_live_xxxxxxxxxxxxxxxxxxxx # Layer 2 — required for user-scoped routes Authorization: Bearer sanctum_token_here
Quick-Start Flow
Route Protection Summary
| Route Group | X-API-Key | Bearer Token | Rate Limit |
|---|---|---|---|
| POST /register | Required | Not needed | 5/min |
| POST /login | Required | Not needed | 5/min |
| POST /auth/forgot-password | Required | Not needed | 5/min |
| POST /auth/reset-password | Required | Not needed | 5/min |
| POST /files (upload) | Required | Required | 10/min |
| POST /events | Required | Required | 100/min |
| POST /events/batch | Required | Required | 20/min |
| All other /api/v1 routes | Required | Required | 60/min |
API Keys
Every request to FireStack must include an X-API-Key header. API keys are scoped to an App, which belongs to a Project. Your key determines which project's data is accessible.
How to Get Your API Key
Sign in to the dashboard and click New Project. This is the top-level container for all your data.
Inside your project, open the Apps tab and click New App. Choose a platform (Android, iOS, Web, Flutter).
An amber notice will show your API Key and API Secret exactly once. Copy them immediately.
Open the App detail page and click Generate New Key to invalidate the old one and receive a new key.
Using Your Key
X-API-Key: fsk_live_xxxxxxxxxxxxxxxxxxxx Authorization: Bearer your_user_token_here # required for authenticated routes
Key Properties
| Property | Details |
|---|---|
| Prefix | fsk_ followed by a random string |
| Shown once | Plain-text key is displayed once at creation; stored as SHA-256 hash |
| Scope | Scoped to one App — different apps have different keys |
| Environment | Keys can be scoped to production, staging, or development |
| Expiration | Optional — keys can have an expiration date |
| Revocation | Generating a new key immediately invalidates the previous one |
Security tip: Never expose your API key in client-side JavaScript or commit it to version control. Use environment variables or a secrets manager.
Register
/api/v1/register
Register a new app user and receive an API token. Only requires the X-API-Key header (no Bearer token needed). Each app has its own isolated user base.
🔐 Multi-Tenant Auth (v3.0): Users are scoped to your app_uuid. The same email can register in different projects independently.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| app_uuid | string (UUID) | Yes | Your app UUID from the dashboard |
| string | Yes | Valid email address (unique per project) | |
| password | string | Yes | Min 8 characters |
| name | string | No | Full name (max 255) |
Example Request
curl -X POST https://your-domain.com/api/v1/register \ -H "Content-Type: application/json" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -d '{ "app_uuid": "550e8400-e29b-41d4-a716-446655440000", "email": "jane@example.com", "password": "secret1234", "name": "Jane Doe" }'
Response
{
"success": true,
"user": {
"id": 42,
"project_id": 1,
"app_uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Jane Doe",
"email": "jane@example.com",
"status": "active",
"custom_claims": {},
"metadata": null
},
"token": "1|AbCdEfGhIjKlMnOpQrStUvWxYz...",
"message": "Registration successful"
}
Login
/api/v1/login
Authenticate an existing app user and receive a Bearer token. Rate limited to 5 attempts per minute.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| app_uuid | string | Yes | Your app UUID from the dashboard |
| string | Yes | Registered email in this app | |
| password | string | Yes | Account password |
Example
curl -X POST https://your-domain.com/api/v1/login \ -H "Content-Type: application/json" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -d '{ "app_uuid": "550e8400-e29b-41d4-a716-446655440000", "email": "jane@example.com", "password": "secret1234" }'
Response
{
"success": true,
"user": {
"id": 42,
"project_id": 1,
"app_uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Jane Doe",
"email": "jane@example.com",
"status": "active"
},
"token": "2|XyZaBcDeFgHiJkLmNoPqRsTuVwX...",
"message": "Login successful"
}
Logout
/api/v1/logout
Revoke the current user's Bearer token. The token can no longer be used after this call.
curl -X POST https://your-domain.com/api/v1/logout \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
{ "success": true, "message": "Logged out successfully" }
Profile
Get Profile
/api/v1/me
curl https://your-domain.com/api/v1/me \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
{
"id": 42,
"project_id": 1,
"app_uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Jane Doe",
"email": "jane@example.com",
"status": "active",
"custom_claims": { "role": "premium" },
"metadata": { "onboarding_complete": true },
"last_login_at": "2026-05-02T10:30:00Z"
}
Update Profile
/api/v1/me
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | No | Display name (max 255) |
| phone | string | No | Phone number (max 20) |
| avatar | string | No | Avatar URL (max 255) |
curl -X PUT https://your-domain.com/api/v1/me \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "name": "Jane Smith", "phone": "+1234567890" }'
Device Tokens
/api/v1/auth/devices
Register a device for Firestack Relay, standards Web Push, or direct APNs delivery.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| transport | string | No | relay, webpush, or apns; inferred from platform when omitted |
| token | string | Depends | APNs token, serialized PushSubscription, or an optional relay installation secret |
| subscription | object | Web only | Browser PushSubscription containing endpoint, keys.p256dh, and keys.auth |
| platform | string | Yes | android, ios, or web |
| device_name | string | No | Human-readable device name |
| app_uuid | string | Yes | The app UUID this device belongs to |
curl -X POST https://your-domain.com/api/v1/auth/devices \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "transport": "relay", "platform": "android", "device_id": "install_01JABC...", "device_name": "Pixel 8 Pro", "app_uuid": "550e8400-e29b-41d4-a716-446655440000" }'
Password Reset
Two-step password reset flow. Only requires X-API-Key (no Bearer token).
Step 1 — Request Reset
/api/v1/auth/forgot-password
| Field | Type | Required |
|---|---|---|
| string | Yes |
curl -X POST https://your-domain.com/api/v1/auth/forgot-password \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "email": "jane@example.com" }'
Step 2 — Reset Password
/api/v1/auth/reset-password
| Field | Type | Required |
|---|---|---|
| token | string | Yes |
| string | Yes | |
| password | string | Yes |
| password_confirmation | string | Yes |
List Collections
/api/v1/collections
Returns a paginated list of all top-level collections in the project associated with your API key.
curl "https://your-domain.com/api/v1/collections?page=1" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
{
"success": true,
"data": [
{
"id": 1,
"name": "users",
"slug": "users",
"document_count": 142
}
],
"meta": { "current_page": 1, "total": 8, "per_page": 15 }
}
Create Collection
/api/v1/collections
Create a new top-level collection. The slug is auto-generated from the name if not provided.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Collection name |
| description | string | No | Collection description |
| schema | object | No | Optional JSON schema for document validation |
| is_public | boolean | No | Whether collection allows unauthenticated reads |
curl -X POST https://your-domain.com/api/v1/collections \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "name": "Products", "description": "Product catalog" }'
Other Collection Endpoints
/api/v1/collections/{slug}
Get collection details
/api/v1/collections/{slug}
Update collection name, description, or schema
/api/v1/collections/{slug}
Delete collection and all its documents
Documents
CRUD operations for documents within a collection. Documents store arbitrary JSON data encrypted at rest with per-project AES-256-CBC encryption.
Collection-Scoped (by slug & doc_id)
/api/v1/collections/{slug}/documents
List documents with optional query filters
/api/v1/collections/{slug}/documents
Create a new document (optional doc_id in body)
/api/v1/collections/{slug}/documents/{docId}
Get document by doc_id within collection
/api/v1/collections/{slug}/documents/{docId}
Replace document data entirely
UUID-Scoped (globally unique)
/api/v1/documents/{uuid}
Fetch document by UUID
/api/v1/documents/{uuid}
Full replace — replaces entire data field
/api/v1/documents/{uuid}
Merge — partial update (deep merge into existing data)
/api/v1/documents/{uuid}
Delete document and cascade to subcollections
Create Document Example
curl -X POST https://your-domain.com/api/v1/collections/products/documents \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "data": { "name": "Widget Pro", "price": 29.99, "tags": ["gadget", "sale"] } }'
Querying
The documents list endpoint supports rich Firestack-style filtering, ordering, and pagination via query parameters.
/api/v1/collections/{slug}/documents
Simple Equality Filter
/api/v1/collections/products/documents?where[status]=active
Operator-Based Filter
/api/v1/collections/products/documents?where[price][op]=lte&where[price][value]=50
| Operator | Meaning | Example |
|---|---|---|
| eq | Equal to (default) | active |
| ne | Not equal to | inactive |
| lt | Less than | 100 |
| lte | Less than or equal | 100 |
| gt | Greater than | 0 |
| gte | Greater than or equal | 18 |
| like | SQL LIKE pattern | %smith% |
| in | Value in array | [1,2,3] |
| not-in | Value not in array | [4,5] |
| array-contains | JSON array contains value | admin |
Ordering & Pagination
| Parameter | Type | Default | Description |
|---|---|---|---|
| order_by | string | created_at | Allowed: created_at, updated_at, doc_id |
| order_dir | string | asc | asc or desc |
| page | integer | 1 | Page number |
| per_page | integer | 15 | Results per page (max 100) |
Full Example
curl -G "https://your-domain.com/api/v1/collections/products/documents" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ --data-urlencode "where[price][op]=lte" \ --data-urlencode "where[price][value]=50" \ --data-urlencode "where[status]=active" \ --data-urlencode "order_by=created_at" \ --data-urlencode "order_dir=desc" \ --data-urlencode "per_page=20"
Subcollections
Documents can contain nested subcollections, just like Firestack. Subcollections are created under a parent document's UUID.
/api/v1/documents/{uuid}/collections
List subcollections under a document
/api/v1/documents/{uuid}/collections
Create a new subcollection (body: name)
/api/v1/documents/{uuid}/collections/{slug}/documents
List documents in a subcollection
/api/v1/documents/{uuid}/collections/{slug}/documents
Create document in subcollection
/api/v1/documents/{uuid}/collections/{slug}/documents/{docId}
Get subcollection document by doc_id
/api/v1/documents/{uuid}/collections/{slug}/documents/{docId}
Replace subcollection document
Example: Create a Subcollection
curl -X POST https://your-domain.com/api/v1/documents/abc-123-uuid/collections \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "name": "reviews" }'
Upload File
/api/v1/files
Upload a file using multipart form data. Max size 50MB. Rate limited to 10 uploads per minute. Files count against the project's storage quota.
Request Body (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
| file | file | Yes | The file to upload (max 50MB) |
| visibility | string | No | public or private (default: private) |
| category | string | No | Category tag, e.g. avatar, document |
| metadata | object | No | Custom JSON metadata |
curl -X POST https://your-domain.com/api/v1/files \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -F "file=@/path/to/image.png" \ -F "visibility=public" \ -F "category=avatar"
Response
{
"success": true,
"data": {
"id": 7,
"uuid": "a1b2c3d4-...",
"original_name": "image.png",
"size": 204800,
"mime_type": "image/png",
"visibility": "public",
"checksum": "sha256:abc123..."
}
}
List Files
/api/v1/files
Returns a paginated list of files in the project. Supports filtering by category, MIME type, and visibility.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| category | string | Filter by category (e.g. avatar) |
| mime_type | string | Filter by MIME type (e.g. image/png) |
| visibility | string | public or private |
| page | integer | Page number (default: 1) |
curl "https://your-domain.com/api/v1/files?category=avatar&visibility=public" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
File Metadata
/api/v1/files/{file}
Retrieve metadata for a specific file including name, size, MIME type, visibility, checksum, and custom metadata.
curl https://your-domain.com/api/v1/files/7 \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
Signed URLs
/api/v1/files/{file}/url
Generate a time-limited signed URL for downloading a private file. Default expiry is 60 minutes.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| minutes | integer | 60 | URL expiry in minutes |
curl "https://your-domain.com/api/v1/files/7/url?minutes=120" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
Response
{
"success": true,
"data": {
"url": "https://your-domain.com/storage/signed/...",
"expires_at": "2026-04-18T14:00:00Z"
}
}
Delete File
/api/v1/files/{file}
Permanently delete a file. The project's storage quota usage is automatically decremented.
curl -X DELETE https://your-domain.com/api/v1/files/7 \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
Channels
Channels are containers for real-time messaging. They can be group channels, project-wide channels, or direct message channels. All messaging events are broadcast via Laravel Reverb for real-time updates.
List Channels
/api/v1/channels
Returns channels the authenticated user is a member of.
Create Channel
/api/v1/channels
| Field | Type | Required | Description |
|---|---|---|---|
| project_id | integer | Yes | Project this channel belongs to |
| name | string | Yes | Channel name (max 255) |
| description | string | No | Channel description (max 1000) |
| type | string | No | group or project |
| is_private | boolean | No | Whether channel requires invitation |
| member_ids | array | No | Array of user IDs to add as initial members |
curl -X POST https://your-domain.com/api/v1/channels \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "project_id": 1, "name": "General", "type": "group", "member_ids": [2, 3, 4] }'
Other Channel Endpoints
/api/v1/channels/{channel}
Get channel details
/api/v1/channels/{channel}
Update name, description, avatar, or archive status
/api/v1/channels/{channel}
Delete channel
/api/v1/channels/{channel}/members
List channel members
/api/v1/channels/{channel}/members
Add member (body: user_id, role)
/api/v1/channels/{channel}/members/{userId}
Remove member
/api/v1/channels/{channel}/members/{userId}/role
Update member role (admin/member)
Direct Messages
/api/v1/channels/direct
Create or retrieve a direct message channel between you and another user. If a DM channel already exists, it is returned instead of creating a duplicate.
| Field | Type | Required |
|---|---|---|
| project_id | integer | Yes |
| user_id | integer | Yes |
curl -X POST https://your-domain.com/api/v1/channels/direct \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "project_id": 1, "user_id": 5 }'
Messages
Send and manage messages within channels. Messages support threading via reply_to_id, and all message events are broadcast in real-time.
Send Message
/api/v1/channels/{channel}/messages
| Field | Type | Required | Description |
|---|---|---|---|
| body | string | Yes | Message content (max 10,000 chars) |
| type | string | No | text, image, file, or reply |
| reply_to_id | integer | No | Message ID to reply to (threading) |
| metadata | object | No | Custom JSON metadata |
curl -X POST https://your-domain.com/api/v1/channels/1/messages \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "body": "Hello everyone!", "type": "text" }'
List, Update & Delete
/api/v1/channels/{channel}/messages
List messages (paginated, supports before_id cursor)
/api/v1/messages/{message}
Edit a message (body, metadata)
/api/v1/messages/{message}
Delete a message
Reactions
Add or remove emoji reactions on messages. Reactions are broadcast in real-time to all channel members.
Add Reaction
/api/v1/messages/{message}/reactions
curl -X POST https://your-domain.com/api/v1/messages/42/reactions \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "emoji": "👍" }'
Remove Reaction
/api/v1/messages/{message}/reactions/{emoji}
curl -X DELETE "https://your-domain.com/api/v1/messages/42/reactions/%F0%9F%91%8D" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
Read Receipts & More
Additional messaging features: read receipts, typing indicators, search, mute, and pin.
/api/v1/channels/{channel}/read
Mark all messages in channel as read
/api/v1/channels/{channel}/typing
Broadcast typing indicator (body: is_typing boolean)
/api/v1/channels/{channel}/search
Search messages (query: q, per_page)
/api/v1/channels/{channel}/mute
Toggle channel mute for current user
/api/v1/channels/{channel}/pin
Toggle channel pin for current user
Search Example
curl "https://your-domain.com/api/v1/channels/1/search?q=hello&per_page=20" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
List Notifications
/api/v1/notifications
Returns a paginated list of notifications for the authenticated user. Notifications are created and sent from the web dashboard or admin panel — the API provides read-only access for client apps.
curl "https://your-domain.com/api/v1/notifications" \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
{
"success": true,
"data": [
{
"id": "uuid-here",
"title": "Welcome!",
"body": "Thanks for signing up.",
"status": "sent",
"sent_at": "2026-04-18T10:00:00Z"
}
]
}
Mark as Read
Mark Single Notification
/api/v1/notifications/{notification}/read
curl -X PUT https://your-domain.com/api/v1/notifications/uuid-here/read \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
Mark All as Read
/api/v1/notifications/read-all
curl -X POST https://your-domain.com/api/v1/notifications/read-all \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
Device Tokens
Push notifications are sent to devices registered via POST /api/v1/auth/devices. Notifications are created and dispatched from the web dashboard with targeting options:
| Target Type | Description |
|---|---|
| all | Send to all registered devices in the project |
| topic | Send to devices subscribed to a specific topic |
| user | Send to a specific user's devices |
| device | Send to a specific device token |
Note: Notifications are created and sent from the web dashboard or Filament admin panel, not via the REST API. The API only provides endpoints to list and mark notifications as read.
Get Config
Remote Config lets you manage feature flags and app settings without shipping a new build. Configs are managed from the web dashboard — the API is read-only for client apps.
Fetch All Values
/api/v1/config
Returns all configs for the current app and environment. Pass ?environment=staging to override (default: production).
curl https://your-domain.com/api/v1/config \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
{
"success": true,
"data": [
{ "key": "maintenance_mode", "value": false },
{ "key": "min_app_version", "value": "2.1.0" },
{ "key": "feature_dark_mode", "value": true }
]
}
Config Features
| Feature | Description |
|---|---|
| Types | String, Number, Boolean, JSON |
| Environments | Production, Staging, Development, or All |
| App Scope | Per-app configs or global (null app_uuid) |
| Feature Flags | Toggle features on/off with is_feature_flag |
| Version Targeting | Set min_app_version / max_app_version |
Get Single Key
/api/v1/config/{key}
Fetch a specific config key's value. The value is automatically cast to its declared type (string, number, boolean, or JSON).
curl https://your-domain.com/api/v1/config/min_app_version \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..."
{
"success": true,
"data": {
"key": "min_app_version",
"value": "2.1.0",
"type": "string"
}
}
Log Event
/api/v1/events
Log a custom analytics event. Rate limited to 100 events per minute. Events are associated with the authenticated user and the app identified by the API key.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| event_name | string | Yes | Event identifier (max 255) |
| properties | object | No | Custom event properties |
| session_id | string | No | Session identifier (max 100) |
| platform | string | No | Platform name (e.g. android, ios) |
| app_version | string | No | App version string |
| device_info | object | No | Device metadata (model, OS, etc.) |
curl -X POST https://your-domain.com/api/v1/events \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "event_name": "purchase_completed", "properties": { "product_id": "pro-monthly", "amount": 29.00 }, "platform": "android", "app_version": "2.3.1" }'
Batch Events
/api/v1/events/batch
Log multiple events in a single request (max 100). Rate limited to 20 batch requests per minute. Ideal for buffering events on the client and flushing periodically.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| events | array | Yes | Array of event objects (1–100 items) |
| events[].event_name | string | Yes | Event identifier |
| events[].properties | object | No | Custom properties |
| events[].session_id | string | No | Session ID |
| events[].platform | string | No | Platform name |
curl -X POST https://your-domain.com/api/v1/events/batch \ -H "X-API-Key: fsk_live_xxxxxxxxxxxx" \ -H "Authorization: Bearer 1|AbCdEfGhIjKl..." \ -H "Content-Type: application/json" \ -d '{ "events": [ { "event_name": "page_view", "properties": { "page": "home" } }, { "event_name": "button_click", "properties": { "button": "signup" } } ] }'
Response
{
"success": true,
"data": { "count": 2 },
"message": "2 events logged successfully"
}
Broadcasting Auth
/api/v1/broadcasting/auth
Authenticate for private and presence WebSocket channels. This is called automatically by the Laravel Echo / Pusher client library — you typically don't call this manually.
// Configure Echo to use your API auth endpoint const echo = new Echo({ broadcaster: 'reverb', authEndpoint: '/api/v1/broadcasting/auth', auth: { headers: { 'X-API-Key': apiKey, 'Authorization': `Bearer ${token}` } } });
Channel Types
FireStack broadcasts events on private channels via Laravel Reverb. Subscribe to these channels to receive real-time updates in your client app.
| Channel | Events | Access |
|---|---|---|
| project.{id}.collections | DocumentCreated, DocumentUpdated, DocumentDeleted | Project members |
| project.{id}.storage | FileUploaded, FileDeleted | Project members |
| channel.{id} | MessageSent, MessageUpdated, MessageDeleted, ReactionAdded, ReactionRemoved, UserTyping | Channel members |
| user.{id} | NotificationSent | Authenticated user only |
Example: Listen for New Messages
echo.private(`channel.${channelId}`) .listen('MessageSent', (e) => { console.log('New message:', e.message); }) .listen('UserTyping', (e) => { console.log(e.user.name, 'is typing...'); });
Installation
The firestack package is a complete Flutter/Dart SDK for FireStack — available on pub.dev. It mirrors the Firebase Flutter SDK patterns you already know: auth state streams, Firestack-style queries, FieldValue operations, upload progress callbacks, and realtime listeners with auto-reconnect.
Add the Dependency
dependencies: firestack: ^1.2.0
dart pub get
SDK Capabilities
Requirements
| Requirement | Version |
|---|---|
| Dart SDK | >=3.0.0 <4.0.0 |
| Flutter | 3.x (for Flutter apps) or Dart-only |
Initialization
Initialize Firestack once at app startup with your API key and app ID. All services (auth, Firestack, storage, etc.) are accessed through the app instance.
🆕 SDK v2.0 Breaking Change: The appId parameter is now required. Get your app ID from the FireStack dashboard under your app settings.
Basic Setup
import 'package:firestack/firestack.dart'; void main() async { final app = Firestack.initialize( apiKey: 'fsk_live_xxxxxxxxxxxx', appId: '550e8400-e29b-41d4-a716-446655440000', // REQUIRED: Your app UUID from dashboard baseUrl: 'https://your-domain.com', // default: https://firestack.co.za ); // Access services final auth = app.auth; final Firestack = app.Firestack; final storage = app.storage; final messaging = app.messaging; final notifications = app.notifications; final config = app.remoteConfig; final analytics = app.analytics; final realtime = app.realtime; }
Configuration Options
| Parameter | Type | Default | Description |
|---|---|---|---|
| apiKey | String | Required | Your fsk_ API key |
| appId | String (UUID) | Required | Your app UUID from the dashboard |
| baseUrl | String | https://firestack.co.za | Self-hosted server URL |
| name | String | [default] | Named instance (for multi-project) |
| timeout | Duration | 30s | HTTP request timeout |
| maxRetries | int | 3 | Auto-retry on 429/5xx/timeout |
| logLevel | FirestackLogLevel | none | none, error, info, verbose |
Multiple App Instances
// Default app final app = Firestack.initialize( apiKey: 'fsk_main', appId: '550e8400-e29b-41d4-a716-446655440000', ); // Named secondary app final secondary = Firestack.initialize( apiKey: 'fsk_other', appId: '550e8400-e29b-41d4-a716-446655440001', name: 'secondary', ); // Retrieve later by name final main = Firestack.instance; final other = Firestack.instanceFor(name: 'secondary');
Authentication
Firebase-compatible auth with state streams, token persistence, password management, and account lifecycle. SDK v2.0 features simplified registration and multi-tenant user isolation.
Auth State Stream
final auth = app.auth; // Listen to auth state changes (like Firebase onAuthStateChanged) auth.authStateChanges.listen((user) { if (user != null) { print('Signed in: \${user.name}'); print('App UUID: \${user.appId}, Project ID: \${user.projectId}'); } else { print('Signed out'); } }); // Token persistence (save to secure storage) auth.setTokenPersistence((token) async { await secureStorage.write(key: 'token', value: token); });
Register & Sign In (v2.0)
// Register (simplified - no password confirmation) final user = await auth.signUp( email: 'alice@example.com', password: 'password', name: 'Alice', // Optional ); // Sign in await auth.signIn( email: 'alice@example.com', password: 'password', ); // Restore existing session auth.signInWithToken('existing-bearer-token'); final me = await auth.currentUser();
User Model (v2.0)
final user = await auth.currentUser(); // Basic fields print(user.id); // User ID print(user.projectId); // Project ID (multi-tenancy) print(user.appId); // App UUID (multi-tenancy) print(user.name); // Display name print(user.email); // Email print(user.status); // 'active', 'pending', 'disabled' // Status helpers if (user.isActive) { ... } if (user.isPending) { ... } if (user.isDisabled) { ... } // Custom claims (for role-based access control) if (user.hasClaim('admin')) { print('User is admin'); } final role = user.getClaim<String>('role'); final isPremium = user.getClaim<bool>('premium'); // Metadata (custom user data) final onboarded = user.getMetadata<bool>('onboarding_complete');
Password & Account Management
// Forgot password (sends reset email) await auth.sendPasswordResetEmail(email: 'alice@example.com'); // Change password (authenticated) await auth.changePassword( currentPassword: 'old', newPassword: 'new', newPasswordConfirmation: 'new', ); // Email verification await auth.sendEmailVerification(); // Refresh token final newToken = await auth.refreshToken(); // Delete account await auth.deleteAccount(password: 'password'); // Sign out await auth.signOut();
Firestack
Firestack API with collections, documents, subcollections, queries, FieldValue operations, batch writes, transactions, aggregate queries, collection group queries, and an in-memory document cache.
CRUD Operations
final Firestack = app.Firestack; // Create final doc = await Firestack.collection('users').add({ 'name': 'Alice', 'age': 30, }); // Read final fetched = await Firestack.collection('users').doc('alice').get(); // Update await Firestack.collection('users').doc('alice').update({'age': 31}); // Delete await Firestack.collection('users').doc('alice').delete(); // Check existence final exists = await Firestack.collection('users').doc('alice').exists();
Queries
final results = await Firestack.collection('users').query((q) => q .where('age', isGreaterThan: 25) .where('status', isEqualTo: 'active') .where('tags', arrayContains: 'premium') .where('role', whereIn: ['admin', 'editor']) .where('deleted_at', isNull: true) .orderBy('created_at', descending: true) .select(['name', 'email', 'age']) .search('alice') .limit(10) .page(1)); // Pagination helpers print('Page \${results.currentPage}/\${results.lastPage}'); print('Has next: \${results.hasNextPage}'); final names = results.map((doc) => doc.get<String>('name'));
Query Operators
| Method | Description |
|---|---|
| isEqualTo | Exact match |
| isNotEqualTo | Not equal |
| isLessThan / isLessThanOrEqualTo | Range filters |
| isGreaterThan / isGreaterThanOrEqualTo | Range filters |
| arrayContains | JSON array contains value |
| whereIn / whereNotIn | Value in/not in set |
| isNull | Null check |
| search() | Full-text search |
| select() | Field projection |
| orderBy() | Sort results |
| limit() / page() | Pagination |
FieldValue Operations
await Firestack.collection('users').doc('alice').update({ 'login_count': FieldValue.increment(1), 'tags': FieldValue.arrayUnion(['vip']), 'temp': FieldValue.delete(), 'updated_at': FieldValue.serverTimestamp(), });
Batch Writes & Transactions
// Batch writes (atomic) final batch = Firestack.batch(); batch.set(Firestack.collection('users').doc('bob'), {'name': 'Bob'}); batch.update(Firestack.collection('users').doc('alice'), {'role': 'admin'}); batch.delete(Firestack.collection('users').doc('charlie')); await batch.commit(); // Transactions (read-then-write) await Firestack.runTransaction((tx) async { final doc = await tx.get(Firestack.collection('accounts').doc('alice')); final balance = doc.get<int>('balance') ?? 0; tx.update(Firestack.collection('accounts').doc('alice'), {'balance': balance - 50}); tx.update(Firestack.collection('accounts').doc('bob'), {'balance': FieldValue.increment(50)}); });
Aggregates & Collection Groups
// Aggregate queries final count = await Firestack.collection('users').count(); final totalRevenue = await Firestack.collection('orders').sum('amount'); final avgAge = await Firestack.collection('users').average('age'); // Collection group queries (across all subcollections named 'comments') final allComments = await Firestack.collectionGroup('comments').getDocs();
Document Cache
// Cache-only read (offline-first) final cached = await Firestack.collection('users').doc('alice') .get(source: CacheSource.cache); // Clear cache Firestack.cache.clear();
Storage
Upload and manage files with progress tracking, signed URLs, batch operations, and stream-based upload tasks (like Firebase UploadTask).
Upload with Progress
final storage = app.storage; // Simple upload with callback final file = await storage.upload( filePath: 'photo.jpg', fileBytes: bytes, visibility: 'public', category: 'images', onProgress: (sent, total) => print('\$sent / \$total'), ); // Stream-based upload (like Firebase UploadTask) final task = storage.uploadWithProgress( filePath: 'video.mp4', fileBytes: videoBytes, ); task.onProgress.listen((snap) { print('\${(snap.progress * 100).toStringAsFixed(1)}%'); }); final result = await task.future;
Download URLs & Files
// Signed download URL with expiry final url = await storage.getDownloadUrl('file-uuid', minutes: 30); print('URL: \${url.url}'); print('Expired: \${url.isExpired}'); // List files with pagination final files = await storage.list( category: 'images', page: 1, perPage: 20, );
Batch Delete & Copy
// Delete multiple files await storage.deleteFiles(['uuid1', 'uuid2']); // Copy a file final copy = await storage.copyFile('uuid', visibility: 'private');
Messaging
Full-featured instant messaging: channels, direct messages, reactions, read receipts, typing indicators, search, mute, and pin.
final messaging = app.messaging; // List channels final channels = await messaging.channels(projectId: 1); // Create group channel final channel = await messaging.createChannel( projectId: 1, name: 'general', memberIds: [2, 3], ); // Create or get direct message channel final dm = await messaging.createDirectChannel( projectId: 1, userId: 2, ); // Send message final msg = await messaging.sendMessage( channelId: channel.id, body: 'Hello!', ); // Edit & delete await messaging.updateMessage(msg.id, body: 'Hello! (edited)'); await messaging.deleteMessage(msg.id); // Reactions await messaging.addReaction(messageId: msg.id, emoji: '\u{1F44D}'); await messaging.removeReaction(messageId: msg.id, emoji: '\u{1F44D}'); // Read receipts & typing await messaging.markAsRead(channel.id); await messaging.sendTyping(channel.id); // Search, mute, pin final results = await messaging.searchMessages(channel.id, query: 'hello'); await messaging.toggleMute(channel.id); await messaging.togglePin(channel.id);
Notifications
Complete push notification lifecycle: permission requests, device token registration, topic subscriptions, foreground message handling, and notification management.
Setup & Permissions
final notifications = app.notifications; // 1. Configure platform & app notifications.configure(platform: 'android', appId: 'com.example.app'); // 2. Set up native permission handler notifications.setPermissionRequestHandler(() async { final status = await Permission.notification.request(); return NotificationSettings( authorizationStatus: status.isGranted ? NotificationAuthorizationStatus.authorized : NotificationAuthorizationStatus.denied, ); }); // 3. Set up device token provider (e.g. from Firebase Messaging) notifications.setTokenProvider(() async { return await FirebaseMessaging.instance.getToken(); }); // 4. Request permission (auto-registers device) final settings = await notifications.requestPermission(); if (settings.isAuthorized) print('Push enabled!');
Topics & Foreground
// Foreground message handling notifications.onMessage((notification) { print('Received: \${notification.title}'); }); // Topic subscriptions await notifications.subscribeToTopic('promotions'); await notifications.unsubscribeFromTopic('promotions');
Manage Notifications
// List (optionally filter to unread) final notifs = await notifications.list(unreadOnly: true); final unread = await notifications.unreadCount(); // Mark read await notifications.markAsRead('notification-id'); await notifications.markAllAsRead(); // Delete await notifications.delete('notification-id'); await notifications.deleteAll();
Remote Config
Firebase-compatible remote config with defaults, fetch throttling, type-safe getters, and feature flag helpers.
final config = app.remoteConfig; // Set defaults (used before first fetch) config.setDefaults({ 'dark_mode': false, 'max_items': 50, 'welcome_msg': 'Hello!', }); // Set minimum fetch interval (throttling) config.minimumFetchInterval = Duration(minutes: 5); // Fetch and activate in one call final updated = await config.fetchAndActivate(); // Type-safe cached getters final darkMode = config.getBool('dark_mode'); final limit = config.getInt('max_items', defaultValue: 50); final ratio = config.getDouble('ratio', defaultValue: 1.0); final name = config.getString('app_name', defaultValue: 'My App'); // Feature flags if (config.isFeatureEnabled('new_ui')) { // show new UI } // Fetch status print(config.lastFetchStatus); // success, failure, throttled print(config.lastFetchTime);
Analytics
Event logging with user identity, user properties, screen tracking, convenience events, and batch support.
final analytics = app.analytics; // Set user identity (like Firebase setUserId) analytics.setUserId('user-123'); // Set user properties (auto-merged into all events) analytics.setUserProperty(name: 'subscription', value: 'premium'); // Set defaults for all events analytics.setDefaults(platform: 'android', appVersion: '2.0.0'); // Screen tracking await analytics.logScreenView( screenName: 'Home', screenClass: 'HomePage', ); // Convenience events await analytics.logLogin(method: 'email'); await analytics.logSignUp(method: 'google'); // Custom events await analytics.logEvent( name: 'purchase', properties: {'item_id': '42', 'price': '9.99'}, ); // Batch events (max 100) await analytics.logBatch([ AnalyticsEvent(name: 'item_view', properties: {'item_id': '42'}), AnalyticsEvent(name: 'add_to_cart', properties: {'qty': '1'}), ]);
Realtime
WebSocket-based realtime with auto-reconnect, heartbeat, Firestack snapshot streams, and message streams — powered by Laravel Reverb.
Connect
final realtime = app.realtime; realtime.configure( host: 'your-domain.com', port: 8080, scheme: 'ws', // or 'wss' for SSL appKey: 'reverb-app-key', appSecret: 'reverb-app-secret', ); await realtime.connect(); // Monitor connection state realtime.stateStream.listen((state) => print('\$state'));
Document & Message Events
// Listen for document changes await realtime.onDocumentCreated(projectId, (data) { print('New document: \$data'); }); await realtime.onDocumentUpdated(projectId, (data) => print(data)); // Listen for messaging events await realtime.onMessageReceived(channelId, (data) => print(data)); await realtime.onTyping(channelId, (data) => print(data));
Firestack Snapshot Streams
// Collection snapshots (like Firebase onSnapshot) Firestack.collection('users').snapshots(projectId: 1).listen((docs) { print('\${docs.length} users'); }); // Single document snapshots Firestack.collection('users').doc('alice').snapshots(projectId: 1).listen((doc) { print('Alice: \${doc?.data}'); }); // Raw streams realtime.snapshotStream(projectId: 1, event: 'document.created').listen(print); realtime.messageStream(channelId).listen(print);
Error Handling
All SDK methods throw FirestackException on failure, with typed helpers for common error scenarios.
try { await app.auth.signIn( email: 'wrong@email.com', password: 'wrong', ); } on FirestackException catch (e) { print('\${e.statusCode}: \${e.message}'); if (e.isUnauthorized) print('Bad credentials'); if (e.isRateLimited) print('Too many requests — retry after \${e.statusCode}'); if (e.isValidationError) print('Errors: \${e.errors}'); if (e.isNotFound) print('Resource not found'); if (e.isServerError) print('Server error'); }
Exception Properties
| Property | Type | Description |
|---|---|---|
| statusCode | int | HTTP status code |
| message | String | Error message from the server |
| errors | Map? | Field-level validation errors |
| isUnauthorized | bool | 401 — invalid credentials or expired token |
| isRateLimited | bool | 429 — too many requests |
| isValidationError | bool | 422 — request body failed validation |
| isNotFound | bool | 404 — resource not found |
| isServerError | bool | 500+ — server error |
Auto-Retry
The SDK automatically retries requests that fail with 429 (rate limited), 5xx (server errors), or timeout — up to maxRetries times (default 3) with exponential backoff. You can configure this with Firestack.initialize(maxRetries: 2).
CLI Tool
The firestack_cli package provides a command-line interface for managing your FireStack backend directly from the terminal.
Installation
# Install from source cd firestack_cli dart pub get # Run directly dart run bin/firestack.dart # Or compile to native binary dart compile exe bin/firestack.dart -o firestack ./firestack
Setup
# Initialize project (creates firestack.json) firestack init --api-key fsk_live_xxx --url https://your-domain.com # Sign in firestack auth login --email alice@example.com --password secret # Check connection firestack status
Available Commands
| Command | Description |
|---|---|
| firestack init | Initialize project configuration |
| firestack status | Check connection and auth status |
| firestack auth login | Sign in |
| firestack auth register | Create a new account |
| firestack auth profile | Show current user profile |
| firestack auth logout | Sign out |
| firestack collections list | List all collections |
| firestack collections create | Create a collection |
| firestack collections show | Show collection details |
| firestack collections delete | Delete a collection |
| firestack docs list | List documents in a collection |
| firestack docs get | Get a document by ID |
| firestack docs add | Add a new document |
| firestack docs set | Set (upsert) a document |
| firestack docs delete | Delete a document |