Documentation
SDKs
React Native

React Native SDK

Installation

npm install @moontraze/react-native

Quick Start

import MoonSDK from '@moontraze/react-native';
 
const moon = new MoonSDK({
  projectId: 'YOUR_PROJECT_ID',
  apiKey: 'YOUR_API_KEY',
});
 
// Auth
const result = await moon.auth.register('user@example.com', 'secret123', 'John');
moon.setToken(result.token!);
 
await moon.auth.login('user@example.com', 'secret123');
moon.auth.logout();
await moon.auth.changePassword('oldSecret', 'newSecret');
 
// Database
await moon.db.set('users/user_123', { name: 'John' });
const user = await moon.db.get('users/user_123');
await moon.db.updateField('users/user_123', 'status', 'verified');
await moon.db.deleteField('users/user_123', 'phone');
await moon.db.delete('users/user_123');
 
// Database Query
const chats = await moon.db.query('chats', {
  where: {
    OR: [
      { fromUid: uid },
      { toUid: uid },
    ],
  },
  orderBy: { createdAt: 'desc' },
  limit: 20,
});
const unread = await moon.db.count('chats', {
  where: { toUid: uid, read: false },
});
 
// Storage
await moon.storage.upload({
  uri: 'file:///path/to/photo.jpg',
  name: 'photo.jpg',
  type: 'image/jpeg',
});
const files = await moon.storage.list();
await moon.storage.createFolder('My Folder');
await moon.storage.delete(fileId);
await moon.storage.deleteFolder(folderId);
const quota = await moon.storage.getQuota();
 
// Notifications
await moon.notifications.registerToken(fcmToken, { platform: 'android' });
await moon.notifications.sendToProject({ title: 'Hello', body: 'World' });
await moon.notifications.sendToUser('user_id', { title: 'Hi', body: 'There' });
await moon.notifications.sendToTopic({ topic: 'premium', title: 'Hi', body: 'There' });
const history = await moon.notifications.getHistory(50);

Authentication

The SDK handles JWT automatically.

  • register() and login() set the token internally.
  • Every subsequent request sends the JWT in the Authorization header automatically.
  • logout() clears the token.

You do not need to manage tokens manually.


Database

Use / for nested paths. Each segment is a collection or document ID.

Create — set

await moon.db.set('courses/js_101/lessons/les_3', {
  title: 'Async/Await',
  duration: 25,
  videoUrl: 'https://cdn.example.com/les3.mp4',
});

Creates:

courses/
  js_101/
    lessons/
      les_3/
        title: "Async/Await"
        duration: 25
        videoUrl: "..."

Important — set replaces the entire document

set() replaces the entire document with the data you provide. If the document already exists, any existing fields that are not included in the new object will be lost.

Use set() for:

  • Creating a new document
  • Replacing a document completely (when you have the full object)

Do NOT use set() for:

  • Updating a single field (e.g. lastSeenAt, status, avatarUrl)
  • Partial updates — use updateField() instead
// BAD — overwrites the whole document
// If `user` object is incomplete, other fields get dropped
const user = await moon.db.get('users/user_123');
user.lastSeenAt = new Date().toISOString();
await moon.db.set('users/user_123', user);
 
// GOOD — updates only lastSeenAt, rest of the document stays intact
await moon.db.updateField(
  'users/user_123',
  'lastSeenAt',
  new Date().toISOString(),
);

Get — get

// Full document
const lesson = await moon.db.get('courses/js_101/lessons/les_3');
// { title: 'Async/Await', duration: 25, videoUrl: '...' }
 
// Specific nested field
const title = await moon.db.get('courses/js_101/lessons/les_3/title');
// 'Async/Await'

Update — updateField

// Single field
await moon.db.updateField('courses/js_101/lessons/les_3', 'duration', 30);
 
// Deeply nested field
await moon.db.updateField('users/user_123', 'address/city/zip', '10001');

updateField updates only the specified field

updateField() updates only the specified field, leaving all other fields in the document untouched. It is the safe way to do partial updates.

Use updateField() for:

  • Updating a single field (e.g. lastSeenAt, status, avatarUrl)
  • Updating a deeply nested field (e.g. address/city/zip)
  • Any change where you want to keep the rest of the document intact
// Updates only the `lastSeenAt` field
await moon.db.updateField(
  'users/user_123',
  'lastSeenAt',
  new Date().toISOString(),
);
 
// Updates a deeply nested field
await moon.db.updateField('users/user_123', 'address/city/zip', '10001');

⚠️ Important — updateField only updates existing fields

updateField() updates an existing field — whether it is top-level or deeply nested. But if the field does not already exist in the document, updateField() will not create it — it fails silently.

Example — when the field exists:

// Existing: users/user_123 = { uid, name, address: { city: 'Mumbai' } }
await moon.db.updateField('users/user_123', 'address/city', 'Delhi');
//  Works — address/city already exists

Example — when the field does not exist:

// Existing: users/user_123 = { uid, name }
await moon.db.updateField('users/user_123', 'address/city', 'Mumbai');
//  Does NOT work — address field does not exist

★ Creating a new field (top-level or nested) — set with merge

To create a new field — whether it is a top-level field or a deeply nested folder — use set() with merge.

Formula:

1. existing = await get(path)
2. merged = { ...existing, ...newFields }
3. await set(path, merged)

Add a new nested folder

// Existing: users/user_123 = { uid, name, phone }
const existing = await moon.db.get('users/user_123');
 
// Create a new nested folder
await moon.db.set('users/user_123', {
  ...existing,                    // ← preserve existing fields
  location: {                     // ← new nested folder
    city: 'Mumbai',
    country: 'India',
  },
});
// Result: users/user_123 = { uid, name, phone, location: { city, country } }

Add a new field inside an existing nested folder

// Existing: users/user_123 = { uid, location: { city: 'Mumbai' } }
const existing = await moon.db.get('users/user_123');
 
await moon.db.set('users/user_123', {
  ...existing,
  location: {
    ...(existing.location ?? {}), // ← preserve existing nested fields
    country: 'India',             // ← new field
  },
});
// Result: users/user_123 = { uid, location: { city: 'Mumbai', country: 'India' } }

Add a new top-level field

// Existing: users/user_123 = { uid, name }
const existing = await moon.db.get('users/user_123');
 
await moon.db.set('users/user_123', {
  ...existing,
  phone: '+91-9876543210',        // ← new top-level field
});
// Result: users/user_123 = { uid, name, phone }

Decision matrix

SituationUse
Field exists, update its valueupdateField()
Add a new top-level fieldset() with merge
Add a new nested folderset() with merge
Add a new field inside an existing nested folderset() with merge (nested spread)
Replace the entire documentset() without merge
Not sure whether the field existsset() with merge (always safe)

Rule of thumb

updateField() = update an existing field set() with merge = create a new field (safe)

If you are not 100% sure whether a field exists, use set() with merge. It is always safe, because ...existing preserves all previous fields.

Delete — deleteField / delete

// Delete a field
await moon.db.deleteField('courses/js_101/lessons/les_3', 'videoUrl');
 
// Delete a document
await moon.db.delete('courses/js_101/lessons/les_3');
 
// Delete a nested collection (own)
await moon.db.delete('courses/js_101/lessons');

★ Query — Filter, Sort, Paginate

Fetch multiple documents from a collection with dynamic filters, sorting, and pagination. No schema required — any field can be queried.

const result = await moon.db.query('chats', {
  where: {
    OR: [
      { fromUid: uid },
      { toUid: uid },
    ],
  },
  orderBy: { createdAt: 'desc' },
  limit: 20,
  offset: 0,
});
 
const results = result.results;    // matching documents
const total = result.total;        // total count
const hasMore = result.hasMore;    // pagination
const took = result.took;          // server time (ms)

Signature

query(collection: string, options?: {
  where?: Record<string, any>;
  orderBy?: Record<string, 'asc' | 'desc' | 'ASC' | 'DESC'>;
  limit?: number;   // default 20, max 500
  offset?: number;  // default 0
}): Promise<{
  results: any[];
  total: number;
  limit: number;
  offset: number;
  hasMore: boolean;
  took?: number;
}>

Filter operators

All operators are optional. Mix and match as needed.

OperatorExampleDescription
Direct equality{ status: 'active' }field = 'active'
eq{ status: { eq: 'active' } }Same as above
neq / ne{ status: { neq: 'banned' } }Not equal
gt{ age: { gt: 18 } }Greater than (numeric)
gte{ age: { gte: 18 } }Greater than or equal
lt{ price: { lt: 1000 } }Less than (numeric)
lte{ price: { lte: 1000 } }Less than or equal
contains{ name: { contains: 'john' } }Case-insensitive substring
startsWith{ name: { startsWith: 'j' } }Prefix match
endsWith{ email: { endsWith: '@gmail.com' } }Suffix match
in{ role: { in: ['admin', 'mod'] } }Value in array
nin{ role: { nin: ['banned'] } }Value not in array
exists{ email: { exists: true } }Field exists / not exists
OR{ OR: [{...}, {...}] }Logical OR
AND{ AND: [{...}, {...}] }Logical AND
NOT{ NOT: {...} }Logical NOT
Nested field{ 'address.city': 'Mumbai' }Dot notation
Null check{ deletedAt: null }IS NULL

Examples

Simple equality:

const result = await moon.db.query('users', {
  where: { role: 'admin' },
});

Multiple conditions (AND implicit):

const result = await moon.db.query('orders', {
  where: {
    status: 'pending',
    amount: { gt: 5000 },
  },
});

OR conditions:

const result = await moon.db.query('chats', {
  where: {
    OR: [
      { fromUid: myUid },
      { toUid: myUid },
    ],
  },
});

Complex nested:

const result = await moon.db.query('posts', {
  where: {
    AND: [
      { status: 'published' },
      {
        OR: [
          { authorUid: myUid },
          { isPublic: true },
        ],
      },
      { createdAt: { gte: '2026-01-01' } },
    ],
  },
  orderBy: { createdAt: 'desc' },
  limit: 50,
});

In array:

const result = await moon.db.query('products', {
  where: {
    category: { in: ['electronics', 'gadgets'] },
    stock: { gt: 0 },
  },
});

Text search:

const result = await moon.db.query('users', {
  where: {
    name: { contains: 'john' },
  },
});

Pagination

// Page 1
const page1 = await moon.db.query('posts', {
  orderBy: { createdAt: 'desc' },
  limit: 20,
  offset: 0,
});
 
// Page 2
const page2 = await moon.db.query('posts', {
  orderBy: { createdAt: 'desc' },
  limit: 20,
  offset: 20,
});
 
// hasMore batata hai agar aur pages hain
if (page1.hasMore) {
  // load next page
}

Sorting

// Single field
const result = await moon.db.query('posts', {
  orderBy: { createdAt: 'desc' },
});
 
// Multiple fields
const result = await moon.db.query('products', {
  orderBy: {
    category: 'asc',
    price: 'desc',
  },
});

Sort directions: 'asc' (or 'ASC'), 'desc' (or 'DESC').


★ Count — Match count without fetching data

Get the number of matching documents without downloading them. Useful for badges, unread counters, dashboard stats.

const unread = await moon.db.count('chats', {
  where: {
    toUid: uid,
    read: false,
  },
});
 
console.log(unread); // 5

Signature

count(collection: string, options?: {
  where?: Record<string, any>;
}): Promise<number>

Returns: number — number of matching documents.

Examples

Total documents in a collection:

const total = await moon.db.count('posts');

Conditional count:

const pendingOrders = await moon.db.count('orders', {
  where: { status: 'pending' },
});

Unread messages in a chat:

const unread = await moon.db.count('chats/conv_123/messages', {
  where: {
    toUid: myUid,
    read: false,
  },
});

Full Example — Chat list (paginated)

async function loadMyChats({
  myUid,
  page = 1,
  pageSize = 20,
}: {
  myUid: string;
  page?: number;
  pageSize?: number;
}) {
  const result = await moon.db.query('chats', {
    where: {
      OR: [
        { participant1: myUid },
        { participant2: myUid },
      ],
    },
    orderBy: { lastMessageAt: 'desc' },
    limit: pageSize,
    offset: (page - 1) * pageSize,
  });
 
  return result.results;
}

Performance notes

  • Indexed queries — dynamic JSONB GIN indexes make where filters fast, even on millions of documents.
  • Always use limit — never fetch unbounded collections. Default is 20, max is 500.
  • Use count() for badges — don't fetch all documents just to count them.
  • Paginate with offset — for infinite scroll, use limit + offset.

Collection Delete — Owner Only

Root collections (users, courses, posts) can only be deleted by the project owner from dashboard. Users can delete their own document and own nested collections only.

PathWho Can Delete
users/{myUid}User (own)
users/{myUid}/devicesUser (own nested)
users (root)Owner only — returns 403

Storage

Upload, manage, and download files. Two upload modes:

  1. Folder-based — upload into a folder (folderId)
  2. Path-based — upload to a specific path (customPath)

Upload

Folder-based (default)

Upload into a folder. Filename is auto-generated (UUID).

await moon.storage.upload(
  {
    uri: 'file:///path/to/photo.jpg',
    name: 'photo.jpg',
    type: 'image/jpeg',
  },
  { folderId: 'folder_abc' },
);
 
// Result:
// {
//   id: 'file_abc',
//   name: 'photo.jpg',
//   publicUrl: 'https://api.moontraze.com/storage/.../uuid.jpg',
//   path: '/{projectId}/{folderName}/uuid.jpg',
// }

Path-based (custom path)

Upload to a specific path with a fixed filename. Useful for user-specific storage like users/{uid}/profile/avatar.jpg.

await moon.storage.upload(
  {
    uri: 'file:///path/to/avatar.jpg',
    name: 'avatar.jpg',
    type: 'image/jpeg',
  },
  { customPath: `users/${uid}/profile/avatar.jpg` },
);
 
// Result:
// {
//   id: 'file_abc',
//   name: 'avatar.jpg',
//   publicUrl: 'https://api.moontraze.com/storage/.../users/.../avatar.jpg',
//   path: '/{projectId}/users/{uid}/profile/avatar.jpg',
// }

Priority

  1. customPath — path-based (highest priority)
  2. folderId — folder-based
  3. Root level — if neither

Backward compatible

Old style still works:

// String folderId
await moon.storage.upload(file, 'folder_abc');

List Files

const files = await moon.storage.list();
const folderFiles = await moon.storage.list('folder_abc');

Delete

await moon.storage.delete(fileId);

Folders

const folder = await moon.storage.createFolder('My Folder');
const folders = await moon.storage.listFolders();
await moon.storage.deleteFolder(folderId);   // recursive

Download

Downloads use presigned URLs — the client fetches directly from MinIO/S3, keeping your server free from bandwidth load.

// Get presigned URL (10 min expiry)
const result = await moon.storage.getPresignedUrl(fileId);
console.log(result.url);         // presigned MinIO URL
console.log(result.expiresIn);   // seconds
 
// Get direct URLs
const downloadUrl = await moon.storage.getDownloadUrl(fileId);
const previewUrl = await moon.storage.getPreviewUrl(fileId);

Presigned URLs work the same for folderId and customPath uploads — both are stored in the same table and served identically.

Quota

const quota = await moon.storage.getQuota();
console.log(quota.usedBytes);
console.log(quota.limitBytes);

Notifications

Push notifications are delivered via your own Firebase project (BYOF). One-time setup from the Moontraze dashboard is required before sending.

One-time Setup

  1. Create your own Firebase project
  2. Add your Android app (matching your applicationId)
  3. Download google-services.json and place it in android/app/
  4. Generate a service account key from Firebase Console
  5. Upload the service account JSON in Moontraze dashboard → Notification Setup

See the Push Notifications Guide for details.

Register FCM Token

await moon.notifications.registerToken(fcmToken, { platform: 'android' });

Unregister FCM Token

await moon.notifications.unregisterToken(fcmToken);

Send to All Users in Project

await moon.notifications.sendToProject({
  title: 'New Feature Released',
  body: 'Check out what we shipped!',
  data: { type: 'announcement' },
});

Send to Specific User

await moon.notifications.sendToUser('user_id', {
  title: 'Order Shipped',
  body: 'Your order is on the way',
  data: {
    type: 'order',
    orderId: '1234',
    deepLink: 'myapp://order/1234',
  },
});

Send to Topic

Devices must subscribe to the topic first:

await messaging().subscribeToTopic('premium_users');
 
await moon.notifications.sendToTopic({
  topic: 'premium_users',
  title: 'Premium Feature',
  body: 'Exclusive for premium users',
});

Schedule for Later

await moon.notifications.schedule(
  {
    title: 'Reminder',
    body: "Don't forget!",
    userId: 'user_abc',      // optional — omit for project-wide
    data: { type: 'reminder' },
  },
  new Date('2026-12-31T10:00:00Z'),
);

Get History

const history = await moon.notifications.getHistory(50);
 
history.forEach((item) => {
  console.log(`${item.title} — ${item.status}`);
  console.log(`Success: ${item.successCount}, Failed: ${item.failureCount}`);
});

Delete History

// Delete a single notification
await moon.notifications.deleteHistory('notification_id');
 
// Clear all history for the current project
await moon.notifications.clearHistory();

Preferences

const prefs = await moon.notifications.getPreferences();
 
await moon.notifications.updatePreferences({
  pushEnabled: true,
  emailEnabled: false,
  marketingEnabled: false,
  securityEnabled: true,
  quietHoursEnabled: true,
  quietHoursStart: '22:00',
  quietHoursEnd: '08:00',
  timezone: 'Asia/Kolkata',
});

Handling Incoming Notifications

The SDK does not manage firebase_messaging stream listeners — you wire them yourself:

// Foreground
messaging().onMessage(async (remoteMessage) => {
  console.log('Foreground:', remoteMessage.notification?.title);
});
 
// Tap when app was in background
messaging().onNotificationOpenedApp((remoteMessage) => {
  const data = remoteMessage.data;
  if (data.type === 'order') {
    navigation.navigate('Order', { orderId: data.orderId });
  }
});
 
// Background handler (must be registered outside component)
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
  console.log('Background message:', remoteMessage);
});

Error: Firebase Not Configured

If a send method is called without Firebase credentials uploaded:

MoonException: Firebase not configured for this project.
Code: FIREBASE_NOT_CONFIGURED

Fix: Upload your service account JSON in Moontraze dashboard → Notification Setup.

Data Payload Notes

  • All data values are auto-converted to strings (FCM requirement)
  • Total payload (title + body + data) must stay under 4 KB
  • Common keys: type, deepLink, imageUrl, plus any app-specific IDs

Registry