Skip to main content

Module System

Understanding Hypermodern Modules

The Hypermodern module system enables you to create reusable, self-contained components that encapsulate related functionality, schemas, and services. Modules promote code reuse, maintainability, and team collaboration by allowing you to package and distribute functionality as discrete units.

What Makes a Module

A Hypermodern module consists of:

  • Module Manifest: Metadata describing the module's identity, dependencies, and exports
  • Schema Definitions: Models, endpoints, and enums specific to the module
  • Service Providers: Business logic and endpoint implementations
  • Migrations: Database schema changes and data transformations
  • Configuration Schema: Configurable parameters and their validation rules
  • Documentation: Usage examples and API documentation

Module Structure

my_module/
├── module.json              # Module manifest
├── schemas/
│   ├── models.json         # Module-specific models
│   ├── endpoints.json      # Module endpoints
│   └── enums.json         # Module enumerations
├── lib/
│   ├── services/          # Business logic
│   ├── providers/         # Service providers
│   ├── middleware/        # Module-specific middleware
│   └── migrations/        # Database migrations
├── config/
│   └── schema.json        # Configuration schema
├── tests/
│   └── module_test.dart   # Module tests
├── examples/
│   └── usage_example.dart # Usage examples
└── README.md              # Module documentation

Creating Custom Modules

Let's create a comprehensive authentication module to demonstrate module development.

Module Manifest

{
  "name": "hypermodern_auth",
  "version": "1.2.0",
  "description": "Complete authentication and authorization module",
  "author": "Your Name <your.email@example.com>",
  "license": "MIT",
  "homepage": "https://github.com/yourorg/hypermodern_auth",
  
  "hypermodern_version": ">=1.0.0 <2.0.0",
  
  "dependencies": {
    "hypermodern_crypto": "^1.0.0",
    "hypermodern_email": "^0.5.0"
  },
  
  "exports": {
    "models": [
      "User",
      "AuthToken", 
      "Permission",
      "Role"
    ],
    "endpoints": [
      "login",
      "logout",
      "register",
      "refresh_token",
      "reset_password",
      "verify_email"
    ],
    "enums": [
      "AuthProvider",
      "TokenType",
      "PermissionLevel"
    ],
    "services": [
      "AuthService",
      "TokenService",
      "PermissionService"
    ]
  },
  
  "configuration": {
    "jwt_secret": {
      "type": "string",
      "required": true,
      "description": "Secret key for JWT token signing"
    },
    "token_expiry": {
      "type": "duration",
      "default": "24h",
      "description": "Default token expiration time"
    },
    "password_min_length": {
      "type": "int32",
      "default": 8,
      "minimum": 6,
      "description": "Minimum password length"
    },
    "enable_email_verification": {
      "type": "bool",
      "default": true,
      "description": "Require email verification for new accounts"
    },
    "oauth_providers": {
      "type": "map<string, object>",
      "default": {},
      "description": "OAuth provider configurations"
    }
  },
  
  "permissions": [
    "auth.login",
    "auth.register", 
    "auth.admin",
    "users.read",
    "users.write",
    "users.delete"
  ],
  
  "database_tables": [
    "users",
    "auth_tokens",
    "roles",
    "permissions",
    "user_roles"
  ]
}

Module Schema Definitions

schemas/models.json:

{
  "models": {
    "user": {
      "id": "int64",
      "email": "string",
      "username": "string",
      "password_hash": "string",
      "email_verified": "bool",
      "email_verification_token": "string?",
      "password_reset_token": "string?",
      "password_reset_expires": "datetime?",
      "last_login": "datetime?",
      "created_at": "datetime",
      "updated_at": "datetime",
      "roles": ["@role"]
    },
    
    "auth_token": {
      "id": "int64",
      "user_id": "int64",
      "token_hash": "string",
      "type": "@token_type",
      "expires_at": "datetime",
      "created_at": "datetime",
      "last_used": "datetime?",
      "metadata": "map<string, any>"
    },
    
    "role": {
      "id": "int64",
      "name": "string",
      "description": "string?",
      "permissions": ["@permission"],
      "created_at": "datetime"
    },
    
    "permission": {
      "id": "int64",
      "name": "string",
      "description": "string?",
      "resource": "string",
      "action": "string"
    }
  }
}

schemas/endpoints.json:

{
  "endpoints": {
    "login": {
      "method": "POST",
      "path": "/auth/login",
      "description": "Authenticate user and return access token",
      "request": {
        "email": "string",
        "password": "string",
        "remember_me": "bool?"
      },
      "response": {
        "user": "@user",
        "access_token": "string",
        "refresh_token": "string",
        "expires_in": "int32"
      },
      "errors": ["invalid_credentials", "account_locked", "email_not_verified"],
      "transports": ["http", "websocket", "tcp"]
    },
    
    "register": {
      "method": "POST", 
      "path": "/auth/register",
      "description": "Create new user account",
      "request": {
        "email": "string",
        "username": "string",
        "password": "string",
        "confirm_password": "string"
      },
      "response": {
        "user": "@user",
        "verification_required": "bool"
      },
      "errors": ["email_exists", "username_exists", "weak_password"],
      "transports": ["http", "websocket", "tcp"]
    },
    
    "refresh_token": {
      "method": "POST",
      "path": "/auth/refresh",
      "description": "Refresh access token using refresh token",
      "request": {
        "refresh_token": "string"
      },
      "response": {
        "access_token": "string",
        "refresh_token": "string",
        "expires_in": "int32"
      },
      "errors": ["invalid_token", "token_expired"],
      "transports": ["http", "websocket", "tcp"]
    }
  }
}

Module Service Implementation

lib/services/auth_service.dart:

import 'package:hypermodern/hypermodern.dart';
import 'package:hypermodern_server/hypermodern_server.dart';
import '../generated/models.dart';

class AuthService {
  final Database db;
  final ModuleConfiguration config;
  final TokenService tokenService;
  final PasswordService passwordService;
  
  AuthService({
    required this.db,
    required this.config,
    required this.tokenService,
    required this.passwordService,
  });
  
  Future<LoginResponse> login(LoginRequest request) async {
    // Find user by email
    final user = await _findUserByEmail(request.email);
    if (user == null) {
      throw InvalidCredentialsException('Invalid email or password');
    }
    
    // Verify password
    final isValidPassword = await passwordService.verify(
      request.password,
      user.passwordHash,
    );
    
    if (!isValidPassword) {
      await _recordFailedLogin(user.id);
      throw InvalidCredentialsException('Invalid email or password');
    }
    
    // Check if email is verified (if required)
    if (config.getBool('enable_email_verification') && !user.emailVerified) {
      throw EmailNotVerifiedException('Please verify your email address');
    }
    
    // Check account status
    if (await _isAccountLocked(user.id)) {
      throw AccountLockedException('Account is temporarily locked');
    }
    
    // Generate tokens
    final tokenExpiry = config.getDuration('token_expiry');
    final accessToken = await tokenService.generateAccessToken(user, tokenExpiry);
    final refreshToken = await tokenService.generateRefreshToken(user);
    
    // Update last login
    await _updateLastLogin(user.id);
    
    // Record successful login
    await _recordSuccessfulLogin(user.id);
    
    return LoginResponse(
      user: user,
      accessToken: accessToken,
      refreshToken: refreshToken,
      expiresIn: tokenExpiry.inSeconds,
    );
  }
  
  Future<RegisterResponse> register(RegisterRequest request) async {
    // Validate passwords match
    if (request.password != request.confirmPassword) {
      throw ValidationException('Passwords do not match');
    }
    
    // Validate password strength
    await passwordService.validateStrength(
      request.password,
      minLength: config.getInt('password_min_length'),
    );
    
    // Check if email already exists
    if (await _emailExists(request.email)) {
      throw EmailExistsException('Email address is already registered');
    }
    
    // Check if username already exists
    if (await _usernameExists(request.username)) {
      throw UsernameExistsException('Username is already taken');
    }
    
    // Hash password
    final passwordHash = await passwordService.hash(request.password);
    
    // Create user
    final user = await db.transaction((tx) async {
      final newUser = await _createUser(tx, User(
        email: request.email,
        username: request.username,
        passwordHash: passwordHash,
        emailVerified: !config.getBool('enable_email_verification'),
        createdAt: DateTime.now(),
        updatedAt: DateTime.now(),
      ));
      
      // Assign default role
      await _assignDefaultRole(tx, newUser.id);
      
      return newUser;
    });
    
    // Send verification email if required
    bool verificationRequired = false;
    if (config.getBool('enable_email_verification')) {
      await _sendVerificationEmail(user);
      verificationRequired = true;
    }
    
    return RegisterResponse(
      user: user,
      verificationRequired: verificationRequired,
    );
  }
  
  Future<RefreshTokenResponse> refreshToken(RefreshTokenRequest request) async {
    // Validate refresh token
    final tokenData = await tokenService.validateRefreshToken(request.refreshToken);
    if (tokenData == null) {
      throw InvalidTokenException('Invalid refresh token');
    }
    
    // Get user
    final user = await _findUserById(tokenData.userId);
    if (user == null) {
      throw InvalidTokenException('User not found');
    }
    
    // Generate new tokens
    final tokenExpiry = config.getDuration('token_expiry');
    final newAccessToken = await tokenService.generateAccessToken(user, tokenExpiry);
    final newRefreshToken = await tokenService.generateRefreshToken(user);
    
    // Revoke old refresh token
    await tokenService.revokeToken(request.refreshToken);
    
    return RefreshTokenResponse(
      accessToken: newAccessToken,
      refreshToken: newRefreshToken,
      expiresIn: tokenExpiry.inSeconds,
    );
  }
  
  // Private helper methods
  Future<User?> _findUserByEmail(String email) async {
    final result = await db.query(
      'SELECT * FROM users WHERE email = ? AND deleted_at IS NULL',
      [email],
    );
    
    return result.isEmpty ? null : User.fromJson(result.first);
  }
  
  Future<bool> _emailExists(String email) async {
    final result = await db.query(
      'SELECT 1 FROM users WHERE email = ? AND deleted_at IS NULL',
      [email],
    );
    
    return result.isNotEmpty;
  }
  
  Future<void> _recordSuccessfulLogin(int userId) async {
    await db.query(
      'INSERT INTO login_attempts (user_id, success, ip_address, created_at) VALUES (?, ?, ?, ?)',
      [userId, true, 'unknown', DateTime.now()],
    );
  }
}

Module Provider

lib/providers/auth_provider.dart:

import 'package:hypermodern_server/hypermodern_server.dart';
import '../services/auth_service.dart';
import '../services/token_service.dart';
import '../services/password_service.dart';

class AuthModuleProvider extends ModuleProvider {
  @override
  String get name => 'hypermodern_auth';
  
  @override
  Future<void> register(ModuleContainer container) async {
    // Register services
    container.singleton<PasswordService>((c) => PasswordService());
    
    container.singleton<TokenService>((c) => TokenService(
      database: c.get<Database>(),
      jwtSecret: c.config.getString('jwt_secret'),
    ));
    
    container.singleton<AuthService>((c) => AuthService(
      db: c.get<Database>(),
      config: c.config,
      tokenService: c.get<TokenService>(),
      passwordService: c.get<PasswordService>(),
    ));
  }
  
  @override
  Future<void> boot(ModuleContainer container) async {
    final server = container.get<HypermodernServer>();
    final authService = container.get<AuthService>();
    
    // Register endpoints
    server.registerEndpoint<LoginRequest, LoginResponse>(
      'login',
      (request) => authService.login(request),
    );
    
    server.registerEndpoint<RegisterRequest, RegisterResponse>(
      'register',
      (request) => authService.register(request),
    );
    
    server.registerEndpoint<RefreshTokenRequest, RefreshTokenResponse>(
      'refresh_token',
      (request) => authService.refreshToken(request),
    );
    
    // Register middleware
    server.middleware.add(AuthenticationMiddleware(
      tokenService: container.get<TokenService>(),
    ));
  }
  
  @override
  List<Migration> get migrations => [
    CreateUsersTableMigration(),
    CreateAuthTokensTableMigration(),
    CreateRolesTableMigration(),
    CreatePermissionsTableMigration(),
  ];
}

Module Manifests and Configuration

Module manifests define the module's identity, dependencies, and configuration schema.

Advanced Manifest Features

{
  "name": "hypermodern_payments",
  "version": "2.1.0",
  "description": "Payment processing module with multiple provider support",
  
  "tags": ["payments", "stripe", "paypal", "billing"],
  "keywords": ["payment", "billing", "subscription", "invoice"],
  
  "compatibility": {
    "hypermodern": ">=1.2.0 <3.0.0",
    "dart": ">=3.0.0 <4.0.0"
  },
  
  "dependencies": {
    "hypermodern_auth": "^1.0.0",
    "hypermodern_notifications": "^0.8.0"
  },
  
  "optional_dependencies": {
    "hypermodern_analytics": "^1.0.0"
  },
  
  "peer_dependencies": {
    "hypermodern_users": "^2.0.0"
  },
  
  "configuration": {
    "default_currency": {
      "type": "string",
      "default": "USD",
      "enum": ["USD", "EUR", "GBP", "JPY"],
      "description": "Default currency for transactions"
    },
    
    "payment_providers": {
      "type": "object",
      "required": true,
      "properties": {
        "stripe": {
          "type": "object",
          "properties": {
            "secret_key": {"type": "string", "required": true},
            "webhook_secret": {"type": "string", "required": true},
            "enabled": {"type": "bool", "default": true}
          }
        },
        "paypal": {
          "type": "object",
          "properties": {
            "client_id": {"type": "string", "required": true},
            "client_secret": {"type": "string", "required": true},
            "sandbox": {"type": "bool", "default": false}
          }
        }
      }
    },
    
    "webhook_endpoints": {
      "type": "array",
      "items": {"type": "string"},
      "default": [],
      "description": "External webhook URLs to notify on payment events"
    },
    
    "retry_policy": {
      "type": "object",
      "properties": {
        "max_attempts": {"type": "int32", "default": 3, "minimum": 1},
        "backoff_multiplier": {"type": "float64", "default": 2.0},
        "initial_delay": {"type": "duration", "default": "1s"}
      }
    }
  },
  
  "events": [
    "payment.created",
    "payment.succeeded", 
    "payment.failed",
    "subscription.created",
    "subscription.cancelled",
    "invoice.generated"
  ],
  
  "hooks": {
    "pre_install": ["scripts/pre_install.dart"],
    "post_install": ["scripts/setup_webhooks.dart"],
    "pre_uninstall": ["scripts/cleanup.dart"]
  },
  
  "assets": [
    "templates/invoice.html",
    "templates/receipt.html",
    "static/payment_icons/"
  ]
}

Configuration Validation

class ModuleConfiguration {
  final Map<String, dynamic> _config;
  final Map<String, ConfigurationField> _schema;
  
  ModuleConfiguration(this._config, this._schema) {
    _validate();
  }
  
  void _validate() {
    for (final entry in _schema.entries) {
      final key = entry.key;
      final field = entry.value;
      final value = _config[key];
      
      // Check required fields
      if (field.required && value == null) {
        throw ConfigurationException('Required field missing: $key');
      }
      
      // Skip validation for null optional fields
      if (value == null) continue;
      
      // Type validation
      if (!_isValidType(value, field.type)) {
        throw ConfigurationException('Invalid type for $key: expected ${field.type}');
      }
      
      // Range validation
      if (field.minimum != null && value < field.minimum!) {
        throw ConfigurationException('Value for $key below minimum: ${field.minimum}');
      }
      
      if (field.maximum != null && value > field.maximum!) {
        throw ConfigurationException('Value for $key above maximum: ${field.maximum}');
      }
      
      // Enum validation
      if (field.enumValues != null && !field.enumValues!.contains(value)) {
        throw ConfigurationException('Invalid value for $key: must be one of ${field.enumValues}');
      }
      
      // Pattern validation
      if (field.pattern != null && value is String) {
        if (!RegExp(field.pattern!).hasMatch(value)) {
          throw ConfigurationException('Value for $key does not match pattern: ${field.pattern}');
        }
      }
    }
  }
  
  T get<T>(String key) {
    final value = _config[key];
    if (value == null) {
      final field = _schema[key];
      if (field?.defaultValue != null) {
        return field!.defaultValue as T;
      }
      throw ConfigurationException('Configuration key not found: $key');
    }
    return value as T;
  }
  
  String getString(String key) => get<String>(key);
  int getInt(String key) => get<int>(key);
  bool getBool(String key) => get<bool>(key);
  double getDouble(String key) => get<double>(key);
  List<T> getList<T>(String key) => (get<List>(key)).cast<T>();
  Map<String, T> getMap<T>(String key) => (get<Map>(key)).cast<String, T>();
  
  Duration getDuration(String key) {
    final value = getString(key);
    return _parseDuration(value);
  }
  
  Duration _parseDuration(String value) {
    final regex = RegExp(r'^(\d+)(ms|s|m|h|d)$');
    final match = regex.firstMatch(value);
    
    if (match == null) {
      throw ConfigurationException('Invalid duration format: $value');
    }
    
    final amount = int.parse(match.group(1)!);
    final unit = match.group(2)!;
    
    switch (unit) {
      case 'ms': return Duration(milliseconds: amount);
      case 's': return Duration(seconds: amount);
      case 'm': return Duration(minutes: amount);
      case 'h': return Duration(hours: amount);
      case 'd': return Duration(days: amount);
      default: throw ConfigurationException('Unknown duration unit: $unit');
    }
  }
}

Sharing and Distributing Modules

Module Registry

class ModuleRegistry {
  final String registryUrl;
  final HttpClient httpClient;
  
  ModuleRegistry({
    this.registryUrl = 'https://registry.hypermodern.dev',
    required this.httpClient,
  });
  
  Future<List<ModuleInfo>> search({
    String? query,
    List<String>? tags,
    String? author,
    int limit = 20,
    int offset = 0,
  }) async {
    final params = <String, String>{
      'limit': limit.toString(),
      'offset': offset.toString(),
    };
    
    if (query != null) params['q'] = query;
    if (tags != null) params['tags'] = tags.join(',');
    if (author != null) params['author'] = author;
    
    final uri = Uri.parse('$registryUrl/modules/search').replace(
      queryParameters: params,
    );
    
    final response = await httpClient.get(uri);
    if (response.statusCode != 200) {
      throw RegistryException('Search failed: ${response.statusCode}');
    }
    
    final data = jsonDecode(response.body) as Map<String, dynamic>;
    final modules = (data['modules'] as List)
        .map((m) => ModuleInfo.fromJson(m))
        .toList();
    
    return modules;
  }
  
  Future<ModuleInfo> getModuleInfo(String name, {String? version}) async {
    final path = version != null ? '/modules/$name/$version' : '/modules/$name';
    final uri = Uri.parse('$registryUrl$path');
    
    final response = await httpClient.get(uri);
    if (response.statusCode == 404) {
      throw ModuleNotFoundException('Module not found: $name');
    } else if (response.statusCode != 200) {
      throw RegistryException('Failed to get module info: ${response.statusCode}');
    }
    
    final data = jsonDecode(response.body) as Map<String, dynamic>;
    return ModuleInfo.fromJson(data);
  }
  
  Future<void> publishModule(String modulePath, String apiKey) async {
    final manifest = await _loadManifest(modulePath);
    final packageData = await _createPackage(modulePath);
    
    final request = http.MultipartRequest('POST', Uri.parse('$registryUrl/modules'));
    request.headers['Authorization'] = 'Bearer $apiKey';
    request.fields['manifest'] = jsonEncode(manifest.toJson());
    request.files.add(http.MultipartFile.fromBytes(
      'package',
      packageData,
      filename: '${manifest.name}-${manifest.version}.tar.gz',
    ));
    
    final response = await request.send();
    if (response.statusCode != 201) {
      throw RegistryException('Failed to publish module: ${response.statusCode}');
    }
  }
  
  Future<String> downloadModule(String name, String version, String targetPath) async {
    final uri = Uri.parse('$registryUrl/modules/$name/$version/download');
    
    final response = await httpClient.get(uri);
    if (response.statusCode != 200) {
      throw RegistryException('Failed to download module: ${response.statusCode}');
    }
    
    final packagePath = path.join(targetPath, '$name-$version.tar.gz');
    await File(packagePath).writeAsBytes(response.bodyBytes);
    
    // Extract package
    await _extractPackage(packagePath, targetPath);
    
    return path.join(targetPath, name);
  }
}

Module Installation

class ModuleInstaller {
  final ModuleRegistry registry;
  final String modulesPath;
  
  ModuleInstaller({
    required this.registry,
    required this.modulesPath,
  });
  
  Future<void> install(String moduleSpec) async {
    final (name, version) = _parseModuleSpec(moduleSpec);
    
    print('Installing $name${version != null ? '@$version' : ''}...');
    
    // Get module info
    final moduleInfo = await registry.getModuleInfo(name, version: version);
    
    // Check compatibility
    await _checkCompatibility(moduleInfo);
    
    // Resolve dependencies
    final dependencies = await _resolveDependencies(moduleInfo);
    
    // Install dependencies first
    for (final dep in dependencies) {
      if (!await _isModuleInstalled(dep.name, dep.version)) {
        await install('${dep.name}@${dep.version}');
      }
    }
    
    // Download and install module
    final modulePath = await registry.downloadModule(
      moduleInfo.name,
      moduleInfo.version,
      modulesPath,
    );
    
    // Run installation hooks
    await _runInstallationHooks(modulePath, moduleInfo);
    
    // Update module registry
    await _updateInstalledModules(moduleInfo);
    
    print('✅ Successfully installed ${moduleInfo.name}@${moduleInfo.version}');
  }
  
  Future<void> uninstall(String moduleName) async {
    print('Uninstalling $moduleName...');
    
    // Check if module is installed
    if (!await _isModuleInstalled(moduleName)) {
      throw ModuleException('Module not installed: $moduleName');
    }
    
    // Check for dependent modules
    final dependents = await _findDependentModules(moduleName);
    if (dependents.isNotEmpty) {
      throw ModuleException(
        'Cannot uninstall $moduleName: required by ${dependents.join(', ')}',
      );
    }
    
    final modulePath = path.join(modulesPath, moduleName);
    final manifest = await _loadManifest(modulePath);
    
    // Run uninstallation hooks
    await _runUninstallationHooks(modulePath, manifest);
    
    // Remove module files
    await Directory(modulePath).delete(recursive: true);
    
    // Update module registry
    await _removeFromInstalledModules(moduleName);
    
    print('✅ Successfully uninstalled $moduleName');
  }
  
  Future<List<ModuleDependency>> _resolveDependencies(ModuleInfo moduleInfo) async {
    final resolved = <ModuleDependency>[];
    final visited = <String>{};
    
    await _resolveDependenciesRecursive(
      moduleInfo.dependencies,
      resolved,
      visited,
    );
    
    return resolved;
  }
  
  Future<void> _resolveDependenciesRecursive(
    Map<String, String> dependencies,
    List<ModuleDependency> resolved,
    Set<String> visited,
  ) async {
    for (final entry in dependencies.entries) {
      final name = entry.key;
      final versionConstraint = entry.value;
      
      if (visited.contains(name)) {
        continue; // Avoid circular dependencies
      }
      
      visited.add(name);
      
      // Find compatible version
      final version = await _findCompatibleVersion(name, versionConstraint);
      final moduleInfo = await registry.getModuleInfo(name, version: version);
      
      // Recursively resolve dependencies
      await _resolveDependenciesRecursive(
        moduleInfo.dependencies,
        resolved,
        visited,
      );
      
      resolved.add(ModuleDependency(name: name, version: version));
    }
  }
}

Module Lifecycle and Hooks

Lifecycle Events

abstract class ModuleLifecycle {
  Future<void> onInstall(ModuleContext context);
  Future<void> onUninstall(ModuleContext context);
  Future<void> onEnable(ModuleContext context);
  Future<void> onDisable(ModuleContext context);
  Future<void> onUpdate(ModuleContext context, String fromVersion);
  Future<void> onConfigurationChange(ModuleContext context, Map<String, dynamic> oldConfig);
}

class PaymentModuleLifecycle implements ModuleLifecycle {
  @override
  Future<void> onInstall(ModuleContext context) async {
    // Set up database tables
    await context.database.runMigrations();
    
    // Create default configuration
    await _createDefaultConfiguration(context);
    
    // Set up webhook endpoints
    await _setupWebhooks(context);
    
    print('Payment module installed successfully');
  }
  
  @override
  Future<void> onUninstall(ModuleContext context) async {
    // Clean up webhooks
    await _cleanupWebhooks(context);
    
    // Archive payment data (don't delete for compliance)
    await _archivePaymentData(context);
    
    print('Payment module uninstalled');
  }
  
  @override
  Future<void> onEnable(ModuleContext context) async {
    // Start background services
    await _startPaymentProcessor(context);
    await _startWebhookListener(context);
    
    print('Payment module enabled');
  }
  
  @override
  Future<void> onDisable(ModuleContext context) async {
    // Stop background services
    await _stopPaymentProcessor(context);
    await _stopWebhookListener(context);
    
    print('Payment module disabled');
  }
  
  @override
  Future<void> onUpdate(ModuleContext context, String fromVersion) async {
    print('Updating payment module from $fromVersion to ${context.module.version}');
    
    // Run version-specific migrations
    if (_shouldMigrateFrom(fromVersion, '1.0.0')) {
      await _migrateFrom1_0_0(context);
    }
    
    if (_shouldMigrateFrom(fromVersion, '2.0.0')) {
      await _migrateFrom2_0_0(context);
    }
    
    // Update webhook configurations
    await _updateWebhooks(context);
    
    print('Payment module updated successfully');
  }
  
  @override
  Future<void> onConfigurationChange(ModuleContext context, Map<String, dynamic> oldConfig) async {
    final newConfig = context.configuration;
    
    // Check if payment providers changed
    if (oldConfig['payment_providers'] != newConfig.get('payment_providers')) {
      await _reconfigurePaymentProviders(context);
    }
    
    // Check if webhook endpoints changed
    if (oldConfig['webhook_endpoints'] != newConfig.get('webhook_endpoints')) {
      await _updateWebhookEndpoints(context);
    }
    
    print('Payment module configuration updated');
  }
}

Custom Installation Scripts

// scripts/setup_webhooks.dart
import 'dart:io';
import 'package:hypermodern_server/hypermodern_server.dart';

Future<void> main(List<String> args) async {
  final context = ModuleContext.fromArgs(args);
  final config = context.configuration;
  
  print('Setting up payment webhooks...');
  
  // Set up Stripe webhooks
  if (config.has('payment_providers.stripe')) {
    await setupStripeWebhooks(context);
  }
  
  // Set up PayPal webhooks
  if (config.has('payment_providers.paypal')) {
    await setupPayPalWebhooks(context);
  }
  
  print('✅ Webhooks configured successfully');
}

Future<void> setupStripeWebhooks(ModuleContext context) async {
  final stripeConfig = context.configuration.getMap('payment_providers.stripe');
  final secretKey = stripeConfig['secret_key'] as String;
  
  final stripe = StripeClient(secretKey);
  
  // Create webhook endpoint
  final webhook = await stripe.webhookEndpoints.create({
    'url': '${context.serverUrl}/webhooks/stripe',
    'enabled_events': [
      'payment_intent.succeeded',
      'payment_intent.payment_failed',
      'invoice.payment_succeeded',
      'customer.subscription.created',
      'customer.subscription.deleted',
    ],
  });
  
  // Store webhook secret in configuration
  await context.updateConfiguration({
    'payment_providers.stripe.webhook_secret': webhook.secret,
  });
  
  print('Stripe webhook created: ${webhook.id}');
}

Service Provider System

The Hypermodern platform includes a comprehensive service provider system that enables proper lifecycle management and dependency injection for modules. Service providers bridge the gap between module metadata and actual implementation, providing structured initialization and cleanup.

Understanding Service Providers

Service providers follow a structured 3-phase lifecycle that ensures proper dependency management and resource cleanup:

  1. Registration Phase - Register services in the IoC container
  2. Initialization Phase - Initialize services with dependencies
  3. Termination Phase - Clean up resources and terminate gracefully

Creating Service Providers

Service providers extend the ModuleServiceProvider class and implement the three lifecycle methods:

import 'package:hypermodern/modules.dart';

class DatabaseServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
  DatabaseServiceProvider({
    required super.manifest,
    required super.config,
  });

  @override
  Future<void> register() async {
    // Register services in the IoC container
    final connectionString = getConfig<String>('connection_string');
    final maxConnections = getConfigOrDefault<int>('max_connections', 10);

    registerSingleton<DatabaseService>(() => DatabaseService(
      connectionString: connectionString,
      maxConnections: maxConnections,
    ));

    registerSingleton<QueryBuilder>(() => QueryBuilder());
  }

  @override
  Future<void> initialize() async {
    // Initialize services after all providers are registered
    final dbService = resolve<DatabaseService>();
    await dbService.connect();
    
    // Set up connection pool
    await dbService.initializeConnectionPool();
    
    // Run any necessary migrations
    await dbService.runMigrations();
  }

  @override
  Future<void> terminate() async {
    // Clean up resources
    final dbService = tryResolve<DatabaseService>();
    if (dbService != null) {
      await dbService.closeConnections();
      await dbService.cleanup();
    }
  }
}

Service Container and Dependency Injection

The service provider system includes a lightweight IoC container for managing dependencies:

// Register services
ServiceContainer.instance.register<MyService>(() => MyService());
ServiceContainer.instance.singleton<DatabaseService>(() => DatabaseService());

// Resolve services
final service = ServiceContainer.instance.resolve<MyService>();
final db = ServiceLocator.get<DatabaseService>();

// Using the ServiceContainerMixin
class MyServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
  @override
  Future<void> register() async {
    // Register services
    registerSingleton<MyService>(() => MyService());
    registerInstance<Config>(myConfig);
  }

  @override
  Future<void> initialize() async {
    // Resolve services
    final service = resolve<MyService>();
    final config = tryResolve<Config>();
  }
}

Module Manager Integration

The ModuleManager orchestrates the entire module lifecycle:

import 'package:hypermodern/modules.dart';

Future<void> main() async {
  // Create module manager
  final manager = ModuleManager.getInstance();
  await manager.initialize();

  // Load modules with service providers
  await manager.loadModule('database', databaseProvider, 
    configOverrides: {
      'connection_string': 'postgresql://localhost:5432/myapp',
      'max_connections': 20,
    });

  await manager.loadModule('cache', cacheProvider,
    configOverrides: {
      'redis_url': 'redis://localhost:6379',
      'default_ttl_seconds': 3600,
    });

  // Start all modules (register and initialize providers)
  await manager.start();

  // Your application logic here...

  // Stop all modules (terminate providers)
  await manager.stop();
}

Configuration Access in Service Providers

Service providers have type-safe access to module configuration:

class PaymentServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
  @override
  Future<void> register() async {
    // Required configuration
    final apiKey = getConfig<String>('stripe_api_key');
    
    // Optional configuration with defaults
    final timeout = getConfigOrDefault<int>('timeout_seconds', 30);
    
    // Check if configuration exists
    if (hasConfig('webhook_secret')) {
      final webhookSecret = getConfig<String>('webhook_secret');
      // Configure webhook validation
    }

    // Type-safe configuration access
    final retryAttempts = getConfigInt('retry_attempts', defaultValue: 3);
    final enableLogging = getConfigBool('enable_logging', defaultValue: true);
    final endpoints = getConfigList<String>('webhook_endpoints');

    registerSingleton<PaymentService>(() => PaymentService(
      apiKey: apiKey,
      timeout: Duration(seconds: timeout),
      retryAttempts: retryAttempts,
      enableLogging: enableLogging,
    ));
  }
}

Advanced Service Provider Patterns

Multi-Service Registration

class AuthServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
  @override
  Future<void> register() async {
    // Register multiple related services
    registerSingleton<PasswordHasher>(() => BCryptPasswordHasher());
    
    registerSingleton<TokenService>(() => JWTTokenService(
      secret: getConfig<String>('jwt_secret'),
      expiry: getConfigDuration('token_expiry'),
    ));
    
    registerSingleton<AuthService>(() => AuthService(
      passwordHasher: resolve<PasswordHasher>(),
      tokenService: resolve<TokenService>(),
      database: resolve<DatabaseService>(),
    ));
    
    // Register middleware
    registerSingleton<AuthMiddleware>(() => AuthMiddleware(
      tokenService: resolve<TokenService>(),
    ));
  }

  @override
  Future<void> initialize() async {
    // Initialize authentication system
    final authService = resolve<AuthService>();
    await authService.initialize();
    
    // Set up default admin user if configured
    if (getConfigBool('create_admin_user', defaultValue: false)) {
      await authService.createDefaultAdmin();
    }
  }
}

Conditional Service Registration

class NotificationServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
  @override
  Future<void> register() async {
    // Register email service if configured
    if (hasConfig('smtp_host')) {
      registerSingleton<EmailService>(() => SMTPEmailService(
        host: getConfig<String>('smtp_host'),
        port: getConfigInt('smtp_port', defaultValue: 587),
        username: getConfig<String>('smtp_username'),
        password: getConfig<String>('smtp_password'),
      ));
    }
    
    // Register SMS service if configured
    if (hasConfig('twilio_account_sid')) {
      registerSingleton<SMSService>(() => TwilioSMSService(
        accountSid: getConfig<String>('twilio_account_sid'),
        authToken: getConfig<String>('twilio_auth_token'),
      ));
    }
    
    // Register push notification service if configured
    if (hasConfig('firebase_server_key')) {
      registerSingleton<PushNotificationService>(() => FirebasePushService(
        serverKey: getConfig<String>('firebase_server_key'),
      ));
    }
    
    // Register the main notification service
    registerSingleton<NotificationService>(() => NotificationService(
      emailService: tryResolve<EmailService>(),
      smsService: tryResolve<SMSService>(),
      pushService: tryResolve<PushNotificationService>(),
    ));
  }
}

Service Provider Testing

import 'package:test/test.dart';
import 'package:hypermodern/modules.dart';

void main() {
  group('DatabaseServiceProvider', () {
    late DatabaseServiceProvider provider;
    late ServiceContainer container;

    setUp(() {
      container = ServiceContainer.instance;
      container.clear();

      final manifest = ModuleHelper.createManifest(
        name: 'database',
        version: '1.0.0',
        description: 'Database module',
        author: 'Test Author',
        configuration: {
          'connection_string': ModuleHelper.configField(
            type: 'string',
            required: true,
            description: 'Database connection string',
          ),
        },
      );

      final config = ModuleConfiguration(
        createConfigSchema(manifest.configuration),
        {'connection_string': 'postgresql://localhost:5432/testdb'},
      );

      provider = DatabaseServiceProvider(
        manifest: manifest,
        config: config,
      );
    });

    test('should register services during registration phase', () async {
      await provider.register();
      provider.markRegistered();

      expect(container.isRegistered<DatabaseService>(), isTrue);
      expect(provider.isRegistered, isTrue);
    });

    test('should initialize services during initialization phase', () async {
      await provider.register();
      provider.markRegistered();
      await provider.initialize();
      provider.markInitialized();

      final dbService = container.resolve<DatabaseService>();
      expect(dbService.isConnected, isTrue);
      expect(provider.isInitialized, isTrue);
    });

    test('should cleanup during termination', () async {
      await provider.register();
      provider.markRegistered();
      await provider.initialize();
      provider.markInitialized();
      await provider.terminate();
      provider.markTerminated();

      final dbService = container.resolve<DatabaseService>();
      expect(dbService.isConnected, isFalse);
      expect(provider.isTerminated, isTrue);
    });
  });
}

Best Practices for Service Providers

  1. Keep registration lightweight: Only register services, don't initialize them during the registration phase
  2. Use initialize for initialization: Initialize services that depend on other modules during the initialization phase
  3. Implement proper termination: Clean up resources to prevent memory leaks
  4. Use configuration validation: Validate required configuration early
  5. Leverage dependency injection: Use the container instead of global state
  6. Handle errors gracefully: Wrap initialization code in try-catch blocks

Service Provider Error Handling

class RobustServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
  @override
  Future<void> register() async {
    try {
      // Service registration logic
      registerSingleton<MyService>(() => MyService());
    } catch (e) {
      throw ServiceProviderException(
        'Failed to register services in ${manifest.name}',
        moduleName: manifest.name,
        cause: e,
      );
    }
  }

  @override
  Future<void> initialize() async {
    try {
      final service = resolve<MyService>();
      await service.initialize();
    } catch (e) {
      // Log the error
      print('Failed to initialize ${manifest.name}: $e');
      
      // Attempt cleanup
      await _attemptCleanup();
      
      // Re-throw with context
      throw ServiceProviderException(
        'Initialization failed for ${manifest.name}',
        moduleName: manifest.name,
        cause: e,
      );
    }
  }

  @override
  Future<void> terminate() async {
    try {
      await _attemptCleanup();
    } catch (e) {
      // Log termination errors but don't throw
      print('Warning: Cleanup failed for ${manifest.name}: $e');
    }
  }

  Future<void> _attemptCleanup() async {
    final service = tryResolve<MyService>();
    if (service != null) {
      await service.cleanup();
    }
  }
}

Integration with Existing Module System

Service providers work seamlessly with the existing module system:

// Traditional module provider
class TraditionalAuthProvider extends ModuleProvider {
  @override
  Future<void> register(ModuleContainer container) async {
    // Traditional registration
  }
}

// New service provider approach
class AuthServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
  @override
  Future<void> register() async {
    // Modern service provider registration
  }
}

The service provider system provides a more structured, testable, and maintainable approach to module development while maintaining full compatibility with existing modules.

What's Next

You now understand how to create, configure, and distribute Hypermodern modules. In the next chapter, we'll explore ORM and data management features, including database integration, migrations, and relationship modeling that work seamlessly with the module system.