Creating Plugins
LAMISPlus uses a plugin architecture where each domain feature is an independent microfrontend paired with a backend Spring Boot module. This guide covers creating both halves.
Frontend Plugin (Remote App)
Using the Script
The fastest way to scaffold a new frontend plugin from the ui/ directory:
cd ui
./create-plugin.sh <app-name> [port]
Examples:
./create-plugin.sh radiology 3007 # Creates radiology plugin on port 3007
./create-plugin.sh sync_monitor # Auto-selects next available port
The script automatically:
- Creates the plugin folder from the
template/scaffold - Registers it in Core's plugin registry (
core/src/config/pluginRegistry.ts) - Adds pnpm workspace scripts (
dev:radiology,restart:radiology, etc.) - Runs
pnpm install
After Creation
# Start your new plugin
pnpm dev:radiology
# Restart Core to pick up the new remote
pnpm restart:core
Your plugin is accessible at:
http://localhost:3000/radiology(through Core)http://localhost:3007(standalone)
Plugin Folder Structure
{plugin-name}/
├── src/
│ ├── App.tsx
│ ├── bootstrap.tsx # Async MF boundary (do not edit)
│ ├── routes.tsx # Route definitions with ProtectedRoute gates
│ ├── index.ts # Entry point (do not edit)
│ ├── features/
│ │ └── {feature}/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── services/ # API calls via axiosInstance
│ │ ├── slices/ # Redux slices
│ │ └── types/
│ ├── components/common/ # Reuse Core components where possible
│ ├── config/
│ │ ├── menus/ # sidebar-menu.json
│ │ ├── icons.tsx
│ │ └── constants.ts # API endpoint paths
│ ├── hooks/
│ ├── store/
│ │ └── rootReducer.ts # Combine plugin reducers here
│ └── types/
├── module-federation.config.ts
├── rspack.config.ts
├── tsconfig.json
└── package.json
What Plugins Expose
Every plugin exposes these surfaces to Core:
exposes: {
'./{PluginName}App': './src/App', // Main component
'./menu': './src/config/menus/menu.json',
'./icons': './src/config/icons',
'./routes': './src/routes',
}
What Plugins Consume from Core
import { useAppDispatch, useAppSelector } from 'core/hooks';
import { axiosInstance } from 'core/axios';
import { storage } from 'core/storage';
Never use
localStoragedirectly. Always usecore/storage. Never create a new axios instance, always import fromcore/axios.
Adding Routes
Wrap every route in PluginWrapper + ProtectedRoute:
// routes.tsx
import { PluginWrapper, ProtectedRoute } from '@/components/common';
export const routes: PluginRouteConfig = {
basePath: '/radiology',
allowedRoles: ['ADMIN', 'USER'],
routes: [
{
index: true,
element: (
<PluginWrapper>
<ProtectedRoute requiredPermission="RADIOLOGY_VIEW">
<RadiologyDashboard />
</ProtectedRoute>
</PluginWrapper>
),
},
{
path: 'orders/create',
element: (
<PluginWrapper>
<ProtectedRoute requiredPermission="RADIOLOGY_CREATE">
<CreateRadiologyOrder />
</ProtectedRoute>
</PluginWrapper>
),
},
],
};
Injecting Redux Reducers
Call injectReducers when the plugin initialises:
// routes.tsx or App.tsx
import { injectReducers } from 'core/store';
import { radiologyReducer } from './store/rootReducer';
injectReducers({ radiology: radiologyReducer });
Backend Plugin
1. Create the Maven Module
Add the module to the root pom.xml:
<modules>
<module>api/lamisplus-core</module>
<module>api/lamisplus-radiology</module> <!-- new -->
</modules>
2. Plugin pom.xml
<dependency>
<groupId>io.github.gracemansam</groupId>
<artifactId>lamiplus_common_api</artifactId>
<version>1.0.7</version>
<scope>provided</scope> <!-- provided, not compile, avoids classloader conflicts -->
</dependency>
3. Implement Plugin Interface
@Component
public class RadiologyPlugin implements Plugin {
@Override
public String getPluginId() { return "radiology"; }
@Override
public String getName() { return "Radiology Management"; }
@Override
public String getVersion() { return "1.0.0"; }
@Override
public String getBasePath() { return "radiology"; }
@Override
public List<PluginPermission> getSecurityPermissions() {
return List.of(
createPermission("RADIOLOGY_VIEW", "View radiology orders", "Radiology", "READ"),
createPermission("RADIOLOGY_CREATE", "Create radiology orders", "Radiology", "CREATE"),
createPermission("RADIOLOGY_UPDATE", "Update radiology orders", "Radiology", "UPDATE"),
createPermission("RADIOLOGY_DELETE", "Delete radiology orders", "Radiology", "DELETE")
);
}
}
4. Create Controller
@RestController
@RequestMapping("/api/plugin/radiology")
@RequiredArgsConstructor
public class RadiologyController {
private final RadiologyService radiologyService;
@GetMapping("/orders")
@PreAuthorize("hasAuthority('RADIOLOGY_VIEW')")
public List<RadiologyOrderDTO> getOrders() {
return radiologyService.findAll();
}
@PostMapping("/orders")
@PreAuthorize("hasAuthority('RADIOLOGY_CREATE')")
public RadiologyOrderDTO createOrder(@Valid @RequestBody RadiologyOrderDTO dto) {
return radiologyService.create(dto);
}
}
5. Tenant-Aware Repository
@Repository
public interface RadiologyOrderRepository
extends PluginTenantAwareRepository<RadiologyOrder, UUID> {
// tenant_id filtered automatically
List<RadiologyOrder> findByPatientUuid(UUID patientUuid);
List<RadiologyOrder> findByArchivedOrderByCreatedAtDesc(int archived);
}
6. Declare Permissions in YAML (Alternative)
Instead of (or in addition to) getSecurityPermissions(), you can declare permissions in src/main/resources/plugin-permissions.yml:
pluginId: radiology
permissions:
- permissionCode: RADIOLOGY_VIEW
description: View radiology orders
category: Radiology
action: READ
- permissionCode: RADIOLOGY_CREATE
description: Create radiology orders
category: Radiology
action: CREATE
7. Build and Install
# Build plugin JAR
cd api/lamisplus-radiology
mvn clean package -DskipTests
# Install via API (requires SUPER_ADMIN token)
curl -X POST http://localhost:8789/api/v1/core/plugin/admin/install \
-H "Authorization: Bearer YOUR_SUPER_ADMIN_TOKEN" \
-H "X-Tenant-Id: _system_" \
-F "file=@target/lamisplus-radiology-1.0.0.jar"
8. Enable for a Tenant
curl -X POST http://localhost:8789/api/v1/core/plugin/radiology/enable \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "X-Tenant-Id: hospital_a"
Checklist for a New Plugin
Backend
-
Plugininterface implemented with correctpluginIdandbasePath - All permissions declared (YAML or
getSecurityPermissions()) - Every controller method has
@PreAuthorize("hasAuthority('...')") - All repositories extend
PluginTenantAwareRepository(notJpaRepository) - Multi-step write operations are
@Transactional(Spring annotation, not Jakarta) -
lamiplus_common_apiatprovidedscope only
Frontend
- Plugin registered in
core/src/config/pluginRegistry.ts - Every route wrapped in
<PluginWrapper><ProtectedRoute requiredPermission="..."> - All API calls use
axiosInstancefromcore/axios - Redux reducers injected via
core/store'sinjectReducers - No
localStoragedirect access - No
anytypes on API responses -
catchblocks calltoast.error(getErrorMessage(err)), never silent - Permission codes in frontend match exactly what the backend declares