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}');
}

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.