bloomBUILD•SHIP•BLOOM
.md
Production Patterns

Cookbook & Recipes

Copy-pasteable architecture recipes for real-world full-stack Dart and Flutter applications.

1. DRF-Style CRUD ViewSet with Throttling & Pagination

Mount a full RESTful CRUD interface over any bloom_db model with automatic request serialization, rate limits, and pagination:

bin/server.dart
dart
// bin/server.dart - DRF-Style CRUD ViewSet with Pagination & Rate Throttling
import 'package:bloom_cache/bloom_cache.dart';
import 'package:bloom_db/bloom_db.dart';
import 'package:bloom_framework/bloom_server.dart';
import 'package:bloom_rest/bloom_rest.dart';
import 'models/todo_task.dart';

void mountTodoApi(BloomApiRouter router, DbExecutor db, BloomCache cache) {
  // 1. Declare field whitelist & read-only constraints
  final serializer = BloomModelSerializer<TodoTask>(
    meta: TodoTask.meta,
    fields: BloomFieldSet.all().withReadOnly(['id', 'createdAt']),
  );

  // 2. Configure ViewSet with permission algebra, throttling, and pagination
  final options = BloomViewSetOptions<TodoTask>(
    serializer: serializer,
    pagination: const PageNumberPagination(defaultPageSize: 25, maxPageSize: 100),
    permission: const IsAuthenticated().and(const IsOwnerOrStaff()),
    filterBackends: const [
      BloomExactFilterBackend(['status', 'priority']),
      BloomSearchFilterBackend(['title', 'description']),
    ],
    throttle: BloomThrottle.fromRate(
      scope: 'todos_api',
      rate: '100/minute',
      cache: cache,
    ),
  );

  // 3. Mount CRUD routes: GET /todos, POST /todos, GET /todos/:id, PUT /todos/:id, DELETE /todos/:id
  mountViewSet<TodoTask>(
    router: router,
    basePath: '/api/todos',
    meta: TodoTask.meta,
    fromRow: TodoTask.fromRow,
    getDb: (req) => db,
    options: options,
  );
}

2. JWT Auth & Declarative Permission Algebra

Implement object-level role-based access control combining .and() and .or() operators:

lib/apps/auth/permissions.dart
dart
// lib/apps/auth/permissions.dart - Declarative JWT Auth & RBAC
import 'package:bloom_framework/bloom_server.dart';
import 'package:bloom_rest/bloom_rest.dart';
import 'package:bloom_security/bloom_security.dart';

class IsOwnerOrStaff extends BloomPermission {
  const IsOwnerOrStaff();

  @override
  Future<bool> hasPermission(BloomRequest request) async {
    final user = request.context.get<AuthUser>('auth_user');
    if (user == null) return false;
    return user.isActive;
  }

  @override
  Future<bool> hasObjectPermission(BloomRequest request, dynamic object) async {
    final user = request.context.get<AuthUser>('auth_user');
    if (user == null) return false;
    if (user.isStaff || user.isSuperuser) return true;

    // Check if the current object belongs to the authenticated user
    if (object is OwnableModel) {
      return object.userId == user.id;
    }
    return false;
  }
}

3. Realtime WebSockets & Live Query Invalidation

Set up a real-time pub/sub hub on the server and broadcast cache invalidation events to connected Flutter clients:

lib/realtime/hub.dart
dart
// lib/realtime/hub.dart - Realtime WebSocket Hub & Query Invalidation
import 'dart:io';
import 'package:bloom_realtime/bloom_realtime.dart';

void setupRealtimeServer(HttpServer server) {
  final hub = BloomChannelHub();

  server.listen((HttpRequest request) async {
    if (request.uri.path == '/ws/realtime') {
      if (WebSocketTransformer.isUpgradeRequest(request)) {
        final socket = await WebSocketTransformer.upgrade(request);
        // Automatically manages channel subscriptions & dead-socket pruning
        hub.registerConnection(socket);
      }
    }
  });
}

// Emits an event that prompts client SWR caches to refetch stale queries
void notifyTaskUpdated(BloomChannelHub hub, int listId, int taskId) {
  hub.broadcast('lists:$listId', {
    'action': 'invalidated',
    'entity': 'todo_task',
    'taskId': taskId,
  });
}

4. Resilient Background Worker Queues

Dispatch asynchronous background tasks (emails, reports, webhooks) with configurable exponential retries:

lib/jobs/email_worker.dart
dart
// lib/jobs/email_worker.dart - Background Worker with Exponential Backoff
import 'package:bloom_jobs/bloom_jobs.dart';
import 'package:bloom_mail/bloom_mail.dart';

class SendWelcomeEmailJob extends BloomJob<Map<String, dynamic>> {
  @override
  String get name => 'send_welcome_email';

  @override
  int get maxRetries => 5;

  @override
  Duration get backoff => const Duration(seconds: 10);

  @override
  Future<void> perform(Map<String, dynamic> payload) async {
    final email = payload['email'] as String;
    final name = payload['name'] as String;

    final mailer = inject<BloomMailer>();
    await mailer.send(
      to: email,
      subject: 'Welcome to Bloom!',
      body: 'Hi $name, welcome aboard!',
    );
  }
}

5. Offline-First SWR Cache & Optimistic UI

Persist queries to local device storage, bridge real-time updates, and optimistically apply mutations offline:

lib/features/todos/controllers/todo_controller.dart
dart
// lib/features/todos/controllers/todo_controller.dart - Offline-First Synchronized Cache
import 'package:bloom_framework/bloom_data.dart';
import 'package:bloom_realtime/bloom_realtime.dart';

class TodoController extends BloomController {
  // SWR Query with local SQLite persistence
  late final tasksQuery = createQuery<List<TodoTask>>(
    queryKey: ['todos', 'list', listId],
    queryFn: () => inject<TodoApi>().fetchTasks(listId),
    staleTime: const Duration(minutes: 5),
    persister: inject<BloomStorageAdapter>(),
  );

  // Optimistic Mutation with automatic offline retry queue
  late final toggleTaskMutation = createMutation<TodoTask, int>(
    mutationFn: (taskId) => inject<TodoApi>().toggleComplete(taskId),
    offlineQueue: true,
    onMutate: (taskId) async {
      // Optimistically flip completion state in UI immediately
      final previous = tasksQuery.data.value;
      // ... update local signal cache
      return previous;
    },
    onError: (err, taskId, rollbackData) {
      // Rollback on network failure
      if (rollbackData != null) tasksQuery.data.value = rollbackData;
    },
  );

  @override
  void onInit() {
    // Automatically invalidate query cache when server pushes real-time events
    inject<BloomRealtimeClient>().invalidateQueriesOnBroadcast(
      channel: 'lists:$listId',
      key: ['todos', 'list', listId],
    );
  }
}

6. Dynamic Multi-Theme & Design Token Switcher

Switch design presets (Nova, Vega, Maia) and dark/light modes at runtime:

lib/features/settings/theme_controller.dart
dart
// lib/features/settings/theme_controller.dart - Dynamic Theme Switcher
import 'package:bloom_framework/bloom_core.dart';
import 'package:bloom_ui/bloom_ui.dart';

class ThemeController extends BloomController {
  late final activePreset = createSignal<BloomStylePreset>(BloomStylePreset.nova);
  late final isDarkMode = createSignal<bool>(true);

  void setPreset(BloomStylePreset preset) {
    activePreset.value = preset;
  }

  void toggleDarkMode() {
    isDarkMode.value = !isDarkMode.value;
  }
}