Documentation

Everything you need to build a complete backend with Cubby. Auth, database, storage, realtime, and more — all in Dart.

Installation

Cubby can be used as a standalone CLI or as a dependency in your Dart project. Both approaches give you a complete backend with auth, database, storage, and realtime out of the box.

Option 1: Global CLI

Install Cubby globally and start a server with zero configuration:

terminal
dart pub global activate cubby
cubby start

This starts a fully functional backend on port 8080 with a built-in admin dashboard at /_/ . Use the CLI to manage users, collections, and documents directly from your terminal.

Option 2: Add to your project

For custom routes, hooks, and full programmatic control, add Cubby as a dependency:

pubspec.yaml
dependencies:
  cubby: ^0.1.0

Then create a server entry point:

bin/server.dart
import 'package:cubby/cubby.dart';

void main(List<String> args) async {
  final server = Cubby(
    port: 8080,
    jwtSecret: 'your-secret-key',
  );

  // Create collections, add hooks, register routes...

  await server.run(args);
}

Run your server:

terminal
dart run bin/server.dart start

Configuration

Configure Cubby through the constructor or environment variables. Constructor values take precedence over environment variables.

Constructor Parameters

dart
Cubby(
  port: 8080,                         // Server port
  jwtSecret: 'your-secret-key',        // JWT signing key
  appUrl: 'http://localhost:8080',   // Public URL
  localStorageEnabled: true,            // Enable local file storage
  localStoragePath: 'storage',           // Storage directory
  logLevel: LogLevel.info,             // debug | info | warn | error
  maxFileSize: 50 * 1024 * 1024,       // 50MB
  databasePath: 'database.sqlite',    // SQLite file path
)

Environment Variables

All settings can also be configured via environment variables:

Variable Default Description
CUBBY_PORT 8080 Server port
CUBBY_SECRET_KEY super_secret_jwt_key JWT signing secret
CUBBY_APP_URL http://localhost:8080 Public application URL
CUBBY_LOCAL_STORAGE_ENABLED true Enable local file storage
CUBBY_LOCAL_STORAGE_PATH storage Directory for local file storage
CUBBY_LOG_LEVEL info Log level (debug, info, warn, error)
CUBBY_MAX_FILE_SIZE 52428800 Max upload size in bytes (50MB)
CUBBY_DATABASE_PATH ./database.sqlite SQLite database file path
CUBBY_RATE_LIMIT_MAX 10 Max requests per rate limit window
CUBBY_RATE_LIMIT_WINDOW_SECONDS 60 Rate limit window in seconds
CUBBY_LOG_RETENTION_DAYS 30 Days to retain logs (0 = forever)

Public API

The Cubby instance exposes services for programmatic access to every subsystem:

dart
final server = Cubby();

server.users        // UsersService
server.auth         // AuthService
server.collections  // CollectionsService
server.documents    // DocumentsService
server.storage      // StorageService
server.settings     // SettingsService
server.logs         // LogsService
server.database     // AppDatabase (Drift ORM)
server.realtime     // RealtimeEventBus
server.hooks        // HookRegistry

Collections

Collections are the core data model in Cubby. There are two types: Base collections (read-write tables) and View collections (read-only SQL views).

Base Collections

Base collections are writable tables with automatic id, created_at, and updated_at fields. Define your schema using typed attributes:

dart
await server.collections.createBase(
  name: 'posts',
  attributes: [
    TextAttribute(name: 'title', nullable: false),
    TextAttribute(name: 'body'),
    BoolAttribute(name: 'published', defaultValue: false),
    IntAttribute(name: 'views', defaultValue: 0),
    TextAttribute(name: 'authorId',
      foreignKey: ForeignKey(
        table: 'users', column: 'id',
        onDelete: ForeignKeyAction.cascade,
      ),
    ),
  ],
  indexes: [
    Index(name: 'idx_posts_author', columns: ['authorId']),
  ],
  listRule: 'auth.uid != null',
  createRule: 'auth.uid != null',
  viewRule: 'auth.uid != null',
  updateRule: 'resource.data[authorId] == auth.uid',
  deleteRule: 'resource.data[authorId] == auth.uid',
);

Attribute Types

Six built-in attribute types cover all common data needs:

Type SQL Type Dart Type
TextAttribute TEXT String
IntAttribute INTEGER int
DoubleAttribute REAL double
BoolAttribute INTEGER (0/1) bool
DateAttribute INTEGER (unix) DateTime
JsonAttribute TEXT (JSON) Map / List

All attributes support these options:

dart
TextAttribute(
  name: 'email',
  nullable: false,       // Required field
  unique: true,         // Unique constraint
  defaultValue: '(random_uuid_v7())',
  checkConstraint: 'length(email) > 3',
  foreignKey: ForeignKey(...),
)

View Collections

Views are read-only collections backed by a SQL SELECT query. They must include an id column and are useful for aggregations, joins, and computed data:

dart
await server.collections.createView(
  name: 'post_stats',
  viewQuery: '''
    SELECT
      p.id,
      p.title,
      p.authorId,
      u.name as authorName,
      p.views,
      p.created_at
    FROM posts p
    JOIN users u ON p.authorId = u.id
    WHERE p.published = 1
  ''',
  listRule: 'auth.uid != null',
);

Collections API

Manage collections programmatically through the service:

dart
// List all collections
final all = await server.collections.list();

// Get a specific collection
final posts = await server.collections.getByName('posts');

// Check existence
final exists = await server.collections.exists('posts');

// Update a collection
await server.collections.updateBase(
  name: 'posts',
  listRule: 'true', // Make public
);

// Delete a collection
await server.collections.delete('posts');

Permissions

Cubby uses expression-based permission rules powered by the expressions package. Rules are strings that evaluate to a boolean, providing fine-grained access control for every operation.

Rule Types

Each collection has five permission rules. A null rule denies all access for that operation. The string "true" allows unrestricted access.

Rule Controls Applies To
listRule Listing/querying documents Base & View
viewRule Reading a single document Base & View
createRule Creating new documents Base only
updateRule Updating existing documents Base only
deleteRule Deleting documents Base only

Context Objects

Rules have access to these objects when evaluating:

dart
// auth — current user info
auth.uid            // User ID (null if guest)
auth.isSuperUser    // Admin flag
auth.token          // Bearer token string

// resource — the document being accessed
resource.id         // Document ID
resource.collection // Collection name
resource.data['field'] // Access any field

// request — HTTP request context
request.method      // HTTP method
request.path        // Request path
request.timestamp    // Current time

Example Rules

dart
// Only authenticated users
'auth.uid != null'

// Only the document author
'resource.data[authorId] == auth.uid'

// Admin only
'auth.isSuperUser == true'

// Author OR admin
'resource.data[authorId] == auth.uid || auth.isSuperUser'

// Check a related document
'get(teams, resource.data[teamId]).data[leadId] == auth.uid'

// Public access
'true'

// Deny all access (or use null)
'false'

Available Methods in Rules

Rules support full Dart-like expression syntax. Methods available on values depend on their type:

Type Methods & Properties
String contains(), startsWith(), endsWith(), toLowerCase(), toUpperCase(), trim(), length, isEmpty, isNotEmpty, split(), substring(), replaceAll()
List contains(), length, isEmpty, isNotEmpty, first, last, where(), join(), elementAt()
Map keys, values, containsKey(), containsValue()
DateTime year, month, day, hour, minute, second, weekday, toIso8601String()
num abs(), ceil(), floor(), round(), clamp(), toInt(), toDouble()

Helper Functions

Two helper functions let you query related data within rules:

dart
// Fetch a document from another collection
get('collection', 'documentId') // Returns Document

// Check if a document exists
exists('collection', 'documentId') // Returns bool

File Storage

Cubby provides built-in file storage with two backends: local filesystem and S3-compatible services (AWS S3, MinIO, etc.). Files are organized into buckets, each with their own permission rules.

Buckets

Create and manage storage buckets with permission rules:

dart
await server.storage.createBucket(
  name: 'avatars',
  createRule: 'auth.uid != null', // Auth users can upload
  viewRule: 'true',              // Anyone can view
  deleteRule: 'resource.file.metadata[uploadedBy] == auth.uid',
);

Upload Files

Upload files via the REST API using multipart form data:

http
POST /v1/files/avatars/upload
Content-Type: multipart/form-data

Form fields:
  path: "users/profile.jpg"  // Destination path
  file: (binary)               // File content
  uploadedBy: "user_123"       // Custom metadata

Download Files

http
// By path (with download token)
GET /v1/files/avatars/download/users/profile.jpg?token=abc123

// By file ID
GET /v1/files/avatars/download/file_id_here

// Get a temporary download URL
GET /v1/files/avatars/get-download-url/file_id_here

Programmatic File Operations

dart
// Upload a file
final file = await server.storage.uploadFile(
  bucket: 'avatars',
  path: 'users/profile.jpg',
  data: fileStream,
  mimeType: 'image/jpeg',
  metadata: {'uploadedBy': userId},
);

// List files in a bucket
final result = await server.storage.listFiles(
  bucket: 'avatars',
  path: 'users/',
  limit: 20,
  orderBy: 'created_at DESC',
);

// Get file by ID or path
final file = await server.storage.getFileById(id);
final file = await server.storage.getFileByPath(
  bucket: 'avatars', path: 'users/profile.jpg',
);

S3 Configuration

Connect to any S3-compatible storage service via settings:

dart
await server.settings.update(
  s3: S3Settings(
    endpoint: 'https://s3.amazonaws.com',
    bucket: 'my-app-files',
    region: 'us-east-1',
    accessKey: 'AKIA...',
    secretKey: 'wJal...',
    enabled: true,
  ),
);

Hooks

Hooks let you intercept and modify operations at specific lifecycle points. Use "before" hooks to validate or transform data, and "after" hooks to trigger side effects.

Document Hooks

dart
// Transform data before creating a document
server.hooks.onBeforeDocumentCreate((event) async {
  event.data['slug'] = slugify(event.data['title']);
  event.data['wordCount'] = countWords(event.data['body']);
});

// Send notification after a document is created
server.hooks.onAfterDocumentCreate((event) async {
  await sendNotification(event.result);
});

// Validate updates
server.hooks.onBeforeDocumentUpdate((event) async {
  if (event.data.containsKey('status')) {
    validateStatusTransition(event.data['status']);
  }
});

// Clean up after deletion
server.hooks.onAfterDocumentDelete((event) async {
  await cleanupRelatedData(event.documentId);
});

User Hooks

dart
// Set defaults for new users
server.hooks.onBeforeUserCreate((event) async {
  event.email = event.email.toLowerCase();
});

// Post-registration setup
server.hooks.onAfterUserCreate((event) async {
  await createDefaultWorkspace(event.result.id);
});

Auth Hooks

dart
server.hooks.onBeforeAuthSignIn((event) async {
  // Rate limiting, IP checks, etc.
});

server.hooks.onAfterAuthSignIn((event) async {
  await logSignIn(event.result.user);
});

File Hooks

dart
server.hooks.onBeforeFileUpload((event) async {
  // Validate file type
  if (!allowedTypes.contains(event.mimeType)) {
    throw CubbyException('File type not allowed');
  }
});

server.hooks.onAfterFileUpload((event) async {
  await generateThumbnail(event.result);
});

Collection Hooks

dart
server.hooks.onBeforeCollectionCreate((event) async {
  // Enforce naming conventions
  event.name = event.name.toLowerCase();
});

server.hooks.onAfterCollectionDelete((event) async {
  await cleanupBuckets(event.name);
});

Note: "Before" hooks receive mutable event data — you can modify fields directly. "After" hooks receive immutable results. If a before hook throws an exception, the operation is cancelled.

Custom Routes

Add your own HTTP endpoints alongside the built-in API. Custom routes receive a standard Shelf Request and have access to all Cubby services through request extensions.

Adding Routes

dart
final server = Cubby();

// Simple GET endpoint
server.addRoute(HttpMethod.get, '/api/status', (request) {
  return Response.ok(jsonEncode({'status': 'healthy'}));
});

// Route with path parameters
server.addRoute(HttpMethod.get, '/api/users/<userId>', (request) async {
  final userId = request.url.pathSegments.last;
  final user = await request.users.getById(userId);
  return Response.ok(jsonEncode(user));
});

// POST with JSON body
server.addRoute(HttpMethod.post, '/api/webhook', (request) async {
  final body = jsonDecode(await request.readAsString());
  await processWebhook(body);
  return Response.ok('OK');
});

Accessing Services in Routes

Custom route handlers can access all Cubby services through request extensions:

dart
server.addRoute(HttpMethod.get, '/api/dashboard', (request) async {
  // Authentication info
  final userId = request.userId;
  final isAdmin = request.isSuperUser;

  // Service access
  final docs = await request.documents.list('posts');
  final users = await request.users.list();
  final settings = await request.settings();

  // Direct database access
  final db = request.database;
  return Response.ok(jsonEncode(dashboard));
});

Excluding Routes from Client SDK

Routes meant for internal use only can be excluded from client SDK generation:

dart
server.addRoute(
  HttpMethod.post,
  '/internal/sync',
  handler,
  ignoreForClient: true, // Won't appear in generated client
);

Client SDK Generation

Cubby can generate a fully typed Dart HTTP client from your server routes. The generated client includes all built-in endpoints plus your custom routes, with type-safe method signatures.

Generate the Client

terminal
# Default output directory
dart run bin/server.dart generate

# Custom output path
dart run bin/server.dart generate --output=packages/my_client

Initialize the Client

The generated client package must be added as a dependency in your frontend or consuming project. If it was generated into a local packages/ directory, use a path dependency:

pubspec.yaml
dependencies:
  cubby_client:
    path: packages/cubby_client

Then initialize the client with your server URL. The client must be created before making any API calls:

dart
import 'package:cubby_client/cubby_client.dart';

final client = CubbyClient(baseUrl: 'http://localhost:8080');

// You must call initialize() before making any requests
await client.initialize();

// For Flutter apps, initialize early (e.g. in main)
void main() async {
  final client = CubbyClient(
    baseUrl: 'https://api.myapp.com',
  );
  await client.initialize();
  runApp(MyApp(client: client));
}

Using the Client

dart
final auth = await client.auth.signInWithEmailAndPassword(
  email: 'user@example.com',
  password: 'password',
);

// Use the access token for authenticated requests
final result = await client.documents.list(
  collectionName: 'posts',
  limit: 20
);

print(result.count);      // Total matching documents
print(result.documents); // List<Document>

Filtering

Use the Filter class to build type-safe query filters. Combine multiple conditions with Filter.and() or Filter.or():

dart
// Simple filter
final filter = Filter.where('status', isEqualTo: 'published');

// Combine with AND
final filter = Filter.and([
  Filter.where('status', isEqualTo: 'published'),
  Filter.where('views', isGreaterThan: 100),
]);

// Combine with OR
final filter = Filter.or([
  Filter.where('role', isEqualTo: 'admin'),
  Filter.where('role', isEqualTo: 'editor'),
]);

final result = await client.documents.list(
  collectionName: 'posts',
  filter: filter.build(),
);

Available filter operators:

Operator Description
isEqualTo Exact match (=)
isNotEqualTo Not equal (!=)
isGreaterThan Greater than (>)
isLessThan Less than (<)
isGreaterThanOrEqualTo Greater or equal (>=)
isLessThanOrEqualTo Less or equal (<=)
like SQL LIKE pattern matching
notLike SQL NOT LIKE pattern matching

Ordering

Use the OrderBy class to sort results. Chain addField() to sort by multiple columns:

dart
// Sort by newest first
final order = OrderBy.desc('created_at');

// Sort by multiple fields
final order = OrderBy.desc('created_at')
  .addField('title', direction: SortDirection.asc);

// Combine filter + order + pagination
final result = await client.documents.list(
  collectionName: 'posts',
  filter: Filter.where('published', isEqualTo: true).build(),
  orderBy: OrderBy.desc('created_at').build(),
  limit: 10,
  offset: 0,
);

Route Grouping

All generated client methods are grouped by their path prefix:

dart
// /v1/auth/*      → client.auth.*
// /v1/documents/* → client.documents.*
// /v1/files/*     → client.files.*
// /api/custom/*   → client.custom.*

Realtime

Cubby supports real-time data synchronization via Server-Sent Events (SSE). Subscribe to document changes, file events, or custom channels and receive updates instantly.

Subscribing to Events

Connect to the realtime endpoint with channels you want to listen to:

http
GET /v1/realtime?sessionId=abc&channels=documents:posts:*,documents:comments:*

Channel Patterns

Pattern Receives
documents:<collection>:* All changes in a collection
documents:<collection>:<id> Changes to a specific document
files:<bucket>:* All file changes in a bucket

Event Types

Three document event types are emitted automatically when documents change:

json
// Document created
{
  "event": "document_created",
  "channels": ["documents:posts:*", "documents:posts:abc123"],
  "document": { "id": "abc123", "title": "Hello", ... }
}

// Document updated (includes previous state)
{
  "event": "document_updated",
  "newDocument": { ... },
  "oldDocument": { ... }
}

// Document deleted
{
  "event": "document_deleted",
  "document": { ... }
}

Subscribing with the Client SDK

Use the Channel helper classes to build type-safe channel subscriptions. The subscribe method returns a stream and an unsubscribe callback:

dart
// Subscribe to all changes in a collection
final (stream, unsubscribe) = await client.realtime.subscribe(
  channel: Channel.collection('posts'),
);

// Listen for events
stream.listen((event) {
  print(event); // DocumentCreatedEvent, DocumentUpdatedEvent, etc.
});

// Unsubscribe when done
unsubscribe();

Three channel types are available:

dart
// All changes in a collection
Channel.collection('posts')

// Only specific event types
Channel.collection('posts', type: RealtimeEventType.create)
Channel.collection('posts', type: RealtimeEventType.update)
Channel.collection('posts', type: RealtimeEventType.delete)

// A specific document
Channel.document('posts', 'doc_id_123')

// Custom channel
Channel.custom('notifications:user_123')

Emitting Custom Events

Push custom events to connected clients from your server code:

dart
server.realtime.emit(
  Transport(
    event: CustomRealtimeEvent(
      channels: ['notifications:user_123'],
      data: {'type': 'message', 'text': 'Hello!'},
    ),
  ),
);

Permission-Aware Subscriptions

Realtime subscriptions respect your collection permission rules. Wildcard subscriptions (e.g. documents:posts:*) filter events through the collection's listRule. Single-document subscriptions use the viewRule. Super users receive all events regardless of rules.

Authentication

Cubby provides JWT-based authentication with email/password sign-in, token refresh, and support for OAuth providers.

Email & Password

dart
// Sign in
final auth = await server.auth.signInWithEmailAndPassword(
  email: 'user@example.com',
  password: 'password',
);

auth.accessToken   // JWT for API requests
auth.refreshToken  // Opaque token for refreshing
auth.user           // User object

// Refresh token
final newAuth = await server.auth.refreshToken(auth.refreshToken);

OAuth Providers

Cubby supports these OAuth providers, configurable through the dashboard or settings API:

Google Apple GitHub Facebook LinkedIn Slack Spotify Discord Reddit Twitch

Configure providers via the settings API or dashboard. Each provider requires a client ID and secret:

dart
await server.settings.update(
  oauthProviders: OAuthProviderList(
    google: OAuthProvider(
      clientId: '123.apps.googleusercontent.com',
      clientSecret: 'GOCSPX-...',
    ),
    github: OAuthProvider(
      clientId: 'your-github-client-id',
      clientSecret: 'your-github-client-secret',
    ),
  ),
);

OAuth Authorization Code Flow

The standard web-based OAuth flow works with all providers. Cubby handles the full flow:

http
// 1. Get the authorization URL
POST /v1/auth/oauth2/google?redirectUrl=https://myapp.com/callback

// 2. Redirect user to the returned URL
// 3. User authorizes, provider calls back to Cubby
// 4. Cubby redirects to your redirectUrl with tokens:
//    https://myapp.com/callback#access_token=...&refresh_token=...&user={...}

Tokens are passed in the URL fragment (#) so they are never sent to your server in access logs. Configure allowed redirect URLs in your settings, including custom URL schemes for mobile apps (e.g. myapp://).

Native Sign-In (Google, Apple, Facebook)

For Flutter and native apps, skip the browser redirect flow entirely. Use the native SDK to get an ID token, then pass it directly to Cubby. Cubby verifies the token signature against the provider's public keys (JWKS), validates the audience matches your configured client ID, and checks email verification.

1. Create Google Cloud credentials

Go to the Google Cloud Console and create a new project (or select an existing one). Then set up the OAuth consent screen:

  1. Navigate to APIs & Services > OAuth consent screen
  2. Choose "External" user type and fill in your app name and support email
  3. Under Data Access (Scopes), ensure these are added: openid, .../auth/userinfo.email, .../auth/userinfo.profile
  4. Complete the branding section with your app logo and links

2. Create OAuth client IDs

You need separate credentials for each platform. Go to APIs & Services > Credentials > Create Credentials > OAuth Client ID:

  1. Web application — Add your Cubby server URL to Authorized JavaScript Origins and the callback URL to Authorized Redirect URIs (e.g. https://your-server.com/v1/auth/oauth2/google/callback)
  2. iOS — Enter your app's Bundle ID (e.g. com.example.myapp)
  3. Android — Enter your package name and the SHA-1 fingerprint from your signing key (use keytool -list -v -keystore ~/.android/debug.keystore for debug)

3. Configure Cubby

Pass all client IDs as a comma-separated string with the web client ID first. The web client secret is used for the authorization code flow:

dart
google: OAuthProvider(
  clientId: [
    'WEB_CLIENT_ID.apps.googleusercontent.com',
    'IOS_CLIENT_ID.apps.googleusercontent.com',
    'ANDROID_CLIENT_ID.apps.googleusercontent.com',
  ].join(','),
  clientSecret: 'GOCSPX-your-web-client-secret',
)

Why multiple client IDs? The web client ID is used for the browser-based authorization code flow. Native sign-in validates the ID token audience (aud) against all configured client IDs, so each platform needs its own.

4. Flutter integration

Add the google_sign_in package and configure platform-specific settings:

pubspec.yaml
dependencies:
  google_sign_in: ^6.0.0

On iOS, add your iOS client ID as GIDClientID in ios/Runner/Info.plist and add a URL scheme matching the reversed client ID. On Android, no additional config is needed if your SHA-1 fingerprint is registered.

dart
import 'package:google_sign_in/google_sign_in.dart';

final googleSignIn = GoogleSignIn(
  serverClientId: 'WEB_CLIENT_ID.apps.googleusercontent.com',
);

final googleUser = await googleSignIn.signIn();
final googleAuth = await googleUser?.authentication;
final idToken = googleAuth?.idToken;

final auth = await client.auth.signInWithIdToken(
  provider: IdTokenAuthProvider.google,
  idToken: idToken!,
);

User Management

Create and manage users via CLI or programmatic API:

terminal
# Create a user
cubby users create -e user@example.com -p password123 -n "John"

# Create an admin
cubby users create -e admin@example.com -p password123 -s

# List users
cubby users list --json

# Generate a one-time password
cubby users generate-otp <userId>

Email

Cubby integrates the mailer package for sending emails. Configure your SMTP server through the dashboard or settings API, then send emails from hooks, custom routes, or anywhere in your server code.

Configure SMTP

Set up your mail provider through the settings API:

dart
await server.settings.update(
  mail: MailSettings(
    smtpServer: 'smtp.gmail.com',
    smtpPort: 587,
    username: 'you@gmail.com',
    password: 'your-app-password',
    useSsl: false,
    fromAddress: 'noreply@myapp.com',
    fromName: 'My App',
  ),
);

Send Emails

Use the smtpServer() helper to get a configured SmtpServer instance, then send messages using the mailer package directly:

dart
import 'package:mailer/mailer.dart';
import 'package:mailer/smtp_server.dart';

final smtp = await server.smtpServer();
final settings = await server.settings.get();

final message = Message()
  ..from = Address(
    settings.mail!.fromAddress,
    settings.mail!.fromName,
  )
  ..recipients.add('user@example.com')
  ..subject = 'Welcome to My App'
  ..html = '<h1>Welcome!</h1><p>Thanks for signing up.</p>';

final report = await send(message, smtp);

Send from Custom Routes

In custom route handlers, access the SMTP server through the request object:

dart
server.addRoute(HttpMethod.post, '/api/invite', (request) async {
  final smtp = await request.smtpServer();
  final settings = await request.settings();

  final message = Message()
    ..from = Address(settings.mail!.fromAddress, settings.mail!.fromName)
    ..recipients.add(recipientEmail)
    ..subject = 'You\'re invited!'
    ..html = inviteHtml;

  await send(message, smtp);
  return Response.ok('Invite sent');
});

Built-in Email Features

Cubby includes built-in email flows for authentication. These work automatically once SMTP is configured:

Feature Endpoint Description
OTP Sign-In POST /v1/auth/send-otp Sends a 6-character one-time password via email (expires in 10 minutes)
Password Reset POST /v1/auth/forgot-password Sends a password reset link with a secure token (expires in 60 minutes)

Custom Email Templates

Override the default OTP and password reset email templates with your own HTML. Templates use Mustache syntax for variable interpolation:

dart
await server.settings.update(
  mail: MailSettings(
    smtpServer: 'smtp.example.com',
    smtpPort: 587,
    fromAddress: 'noreply@myapp.com',
    fromName: 'My App',
    otpTemplate: '<h1>Your code: {{otp_code}}</h1>',
    resetPasswordTemplate: '<a href="{{reset_url}}">Reset password</a>',
  ),
);

Available template variables:

Template Variables
OTP {{otp_code}}, {{user.email}}, {{user.name}}, {{user.id}}
Password Reset {{reset_url}}, {{user.email}}, {{user.name}}, {{user.id}}
Cubby Logo Cubby

©2026 Cubby. Built for the Dart & Flutter Community.