- Introduction
- Core Architecture
- Plugin System
- Hook System
- Cloud Functions
- Middleware System
- Parse Server Integration
- ES Module Requirements
- Plugin Development
- Database and Collections
- Capabilities and Roles
- Best Practices & Gotchas
Actinium is a plugin-based backend framework built on top of Parse Server and Express.js by Atomic Reactor. It extends Parse Server with a sophisticated plugin architecture, hook system, and convention-based auto-discovery of backend features.
- Plugin Architecture: Backend features organized as auto-discoverable plugins
- Hook System: Event-driven lifecycle allows plugins to intercept and extend behavior
- Parse Server Extension: Full access to Parse Server's features (MongoDB integration, user authentication, ACLs)
- Cloud Function Gateway: Plugin-based Cloud Functions with automatic permission gating
- Middleware Discovery: Auto-discovered Express middleware from plugin files
- ES Module First: Requires ES module syntax (
import/export), not CommonJS
api/
├── actinium_modules/
│ └── @atomic-reactor/
│ └── actinium-core/ # Framework core
├── src/
│ ├── app/ # Application plugins
│ │ └── my-plugin/
│ │ ├── plugin.js # Plugin registration
│ │ ├── info.js # Plugin metadata
│ │ └── sdk.js # Plugin SDK (optional)
│ └── index.js # Application entry point
├── .env # Environment configuration
└── package.json
Actinium extends the Parse SDK and provides a global Actinium object with extensive utilities:
Location: /api/actinium_modules/@atomic-reactor/actinium-core/actinium.js
// Actinium is a global object available everywhere
Actinium = {
...Parse, // Inherits all Parse SDK methods
// Actinium-specific modules
Hook, // Hook system
Plugin, // Plugin management
Cloud, // Cloud Function registration
Middleware, // Express middleware management
Utils, // Utility functions
Cache, // Caching layer
Setting, // Settings management
Roles, // Role management
Capability, // Capability-based permissions
User, // User utilities
Collection, // MongoDB collection utilities
Type, // Content type management
Pulse, // Pub/sub event system
Enums, // Constants and enums
File, // File handling
FilesAdapter, // File storage adapter
};From: /api/actinium_modules/@atomic-reactor/actinium-core/actinium.js
The Actinium initialization process follows this sequence:
-
Actinium.init(options)- Initializes all core systems:// Express app setup const app = express(); Actinium.app = app; // Express settings initialization Actinium.Exp.init(app, options); // Middleware discovery and registration await Actinium.Middleware.init(app); // Plugin discovery and registration await Actinium.Plugin.init(); // File adapter initialization await Actinium.FilesAdapter.init(); // Settings initialization Actinium.Setting.init(); // Content type initialization Actinium.Type.init(); Actinium.ready = true; // Fire init hook await Actinium.Hook.run('init', app, options);
-
Actinium.start(options)- Starts the HTTP server:// Create HTTP or HTTPS server Actinium.server = http.createServer(Actinium.app); // Start listening on configured PORT Actinium.server.listen(PORT); Actinium.started = true;
Typical /api/src/index.js:
import Actinium from '@atomic-reactor/actinium-core';
(async () => {
await Actinium.init();
await Actinium.start();
})();A typical Actinium plugin consists of several files:
src/app/my-plugin/
├── info.js # Plugin metadata (required)
├── plugin.js # Plugin registration and initialization (required)
├── sdk.js # Plugin SDK methods (optional)
└── my-service.js # Additional service files (optional)
Defines plugin identity and configuration:
// src/app/my-plugin/info.js
const PLUGIN = {
ID: 'MyPlugin', // Unique identifier
name: 'My Plugin', // Display name
description: 'Description of plugin functionality',
version: '1.0.0', // Plugin version
pluginDependencies: [], // Array of plugin IDs this depends on
order: 100, // Load order (lower = earlier)
bundle: [], // Files to bundle
meta: {
group: 'MyPluginGroup', // Grouping for organization
builtIn: false, // true for core plugins
},
};
export default PLUGIN;The main plugin file that registers the plugin and its features:
// src/app/my-plugin/plugin.js
import Actinium from '@atomic-reactor/actinium-core';
import PLUGIN from './info.js';
import SDK from './sdk.js';
const MOD = () => {
// Attach SDK to global Actinium object
Actinium.MyPlugin = Actinium.MyPlugin || SDK;
// Register the plugin
Actinium.Plugin.register(PLUGIN, true); // true = activate immediately
// Register Cloud Functions
Actinium.Cloud.define(PLUGIN.ID, 'myCloudFunction', async (req) => {
const { param1, param2 } = req.params;
return SDK.doSomething(param1, param2);
});
console.log('MyPlugin registered');
};
export default MOD(); // IMPORTANT: Execute immediatelyKey Points:
- Export the result of calling
MOD()immediately, not just the function - Register plugin with
Actinium.Plugin.register(PLUGIN, active) - Attach SDK to
Actiniumobject for global access - Use
Actinium.Cloud.define()for Cloud Functions (notParse.Cloud.define())
Optional SDK exposing plugin methods:
// src/app/my-plugin/sdk.js
import MyService from './my-service.js';
export default {
doSomething: MyService.doSomething.bind(MyService),
anotherMethod: async (param) => {
// Implementation
return { result: 'success' };
},
};From: /api/actinium_modules/@atomic-reactor/actinium-core/lib/plugable.js
Plugins are auto-discovered via globbing patterns defined in ENV.GLOB_PLUGINS:
Actinium.Plugin.init = async () => {
const files = globby(ENV.GLOB_PLUGINS); // e.g., 'src/app/**/plugin.js'
// Import all plugin.js files
await Promise.all(files.map((file) => import(normalizeImportPath(file))));
// Activate plugins based on database settings
await Actinium.Plugin.activate();
};Plugin Activation: Plugins can be activated/deactivated dynamically via the database or configuration.
From: lib/plugable.js
Plugins are validated before registration:
const _isValid = (plugin, strict = false) => {
const { ID } = plugin;
// Must have unique ID
if (!ID || blacklist.includes(ID)) return false;
// Validate Actinium version compatibility
const actiniumVer = ACTINIUM_CONFIG.version;
const versionRange = plugin.version.actinium || `>=${actiniumVer}`;
if (!semver.satisfies(actiniumVer, versionRange)) return false;
// If strict, must be active
if (strict && !Plugable.isActive(ID)) return false;
return true;
};Actinium's hook system is the backbone of its extensibility, allowing plugins to intercept and extend framework behavior.
Location: /api/actinium_modules/@atomic-reactor/actinium-core/lib/hook.js
The Hook system provides:
- Async hooks: Most common, allows
awaitin callbacks - Sync hooks: For synchronous operations
- Priority-based execution: Hooks execute in order of priority
- Action sequence: Uses
action-sequencelibrary for orchestrated async execution
Async Hooks (most common):
import Actinium from '@atomic-reactor/actinium-core';
Actinium.Hook.register(
'my-custom-hook', // Hook name
async (arg1, arg2, context) => {
// Callback
console.log('Hook fired:', arg1, arg2);
// Async operations allowed
const result = await someAsyncOperation();
// Can modify context
context.customData = result;
},
Actinium.Enums.priority.neutral, // Priority
'unique-hook-id' // Optional unique ID
);Sync Hooks:
Actinium.Hook.registerSync(
'my-sync-hook',
(arg1, arg2, context) => {
// Synchronous only, no await
console.log('Sync hook:', arg1);
},
Actinium.Enums.priority.neutral,
'sync-hook-id'
);Run async hooks:
const context = await Actinium.Hook.run('my-custom-hook', 'arg1', 'arg2');
// context = { hook: 'my-custom-hook', params: ['arg1', 'arg2'], customData: ... }Run sync hooks:
const context = Actinium.Hook.runSync('my-sync-hook', 'data');Initialization Hooks (order of execution):
-
warning: Early warnings before initialization -
init: Core initialization complete, app and options availableActinium.Hook.register('init', async (app, options) => { console.log('Actinium initialized'); });
-
start: Server starting -
started: Server started and listening -
activate: Plugin activation -
schema-created: After database schemas created
Request/Response Hooks:
-
before-save-{ClassName}: Before saving a Parse objectActinium.Hook.register('before-save-User', async (req, context) => { const { object, user, master } = req; // Validate or modify object before save if (!object.get('email')) { throw new Error('Email required'); } });
-
after-save-{ClassName}: After saving a Parse object -
before-delete-{ClassName}: Before deleting -
after-delete-{ClassName}: After deleting -
before-find-{ClassName}: Before query execution -
after-find-{ClassName}: After query execution
Cloud Function Hooks:
before-cloud-{functionName}: Before Cloud Function executionafter-cloud-{functionName}: After Cloud Function execution
Custom Application Hooks:
Define your own hooks for plugin communication:
// In plugin A
Actinium.Hook.register('data-processed', async (data, context) => {
console.log('Data processed:', data);
});
// In plugin B
await Actinium.Hook.run('data-processed', processedData);From: lib/enums.js
Actinium.Enums.priority = {
highest: -1000000, // Execute first
core: -1000,
high: -100,
normal: 0, // Default
low: 100,
lowest: 1000000, // Execute last
};Lower numbers = higher priority (execute earlier).
Every hook execution creates a context object:
const context = await Actinium.Hook.run('my-hook', arg1, arg2);
// context = {
// hook: 'my-hook',
// params: [arg1, arg2],
// // Plus any properties added by hook callbacks
// }Hook callbacks can modify the context to pass data between hooks.
const hookId = Actinium.Hook.register('my-hook', callback, priority);
// Later, unregister
Actinium.Hook.unregister(hookId);From: lib/hook.js
Errors in async hooks are caught and logged but don't crash the server:
try {
await ActionSequence({ actions: hookActions, context });
} catch (errors) {
Object.entries(errors).forEach(([id, error]) => {
ERROR(`Error in action.${name}[${id}]`);
if (error.error instanceof assert.AssertionError) {
// Log assertion details
} else {
ERROR(error);
}
});
}Cloud Functions in Actinium are Parse Cloud Functions with additional plugin-based gating and hook integration.
Always use Actinium.Cloud.define(), not Parse.Cloud.define():
// In plugin.js
import Actinium from '@atomic-reactor/actinium-core';
import PLUGIN from './info.js';
import SDK from './sdk.js';
Actinium.Cloud.define(PLUGIN.ID, 'myCloudFunction', async (req) => {
// req.params: Client-provided parameters
// req.user: Authenticated user (if any)
// req.master: Master key usage flag
const { param1, param2 } = req.params;
// Perform operations
const result = await SDK.doSomething(param1, param2);
return result; // Returned to client
});Cloud Function Signature:
Actinium.Cloud.define(
pluginID, // Plugin ID (for permission gating)
functionName, // Cloud Function name
callback // async function(req) => result
);From: lib/cloud.js
Actinium.Cloud.define() wraps Parse.Cloud.define() with plugin gating:
Cloud.define = (plugin, name, callback) => {
if (!plugin || !name || !callback) {
throw new Error(
'Cloud.define(plugin, name, callback) all parameters required'
);
}
Parse.Cloud.define(name, (req) =>
Actinium.Plugin.gate({ req, ID: plugin, name, callback })
);
CLOUD_FUNCTIONS.push({ name });
};Plugin Gating: Ensures the plugin is active before executing the Cloud Function. If plugin is inactive, the function throws an error.
From frontend (Reactium):
import Parse from 'parse';
const result = await Parse.Cloud.run('myCloudFunction', {
param1: 'value1',
param2: 'value2',
});From backend (another plugin):
const result = await Actinium.Cloud.run(
'myCloudFunction',
{
param1: 'value1',
param2: 'value2',
},
{ sessionToken: user.getSessionToken() }
);Intercept Cloud Function execution:
// Before execution
Actinium.Hook.register('before-cloud-myCloudFunction', async (req, context) => {
console.log('About to execute myCloudFunction');
// Can modify req.params
req.params.injectedParam = 'value';
// Can throw to prevent execution
if (!req.user) {
throw new Error('Authentication required');
}
});
// After execution
Actinium.Hook.register(
'after-cloud-myCloudFunction',
async (req, result, context) => {
console.log('myCloudFunction returned:', result);
// Can modify result (if returned from context)
context.result = modifiedResult;
}
);From this project: /api/src/app/coingecko/plugin.js
import Actinium from '@atomic-reactor/actinium-core';
import PLUGIN from './info.js';
import SDK from './sdk.js';
const MOD = () => {
Actinium.CoinGecko = Actinium.CoinGecko || SDK;
Actinium.Plugin.register(PLUGIN, true);
// Register Cloud Function
// Accessible at: POST http://localhost:9000/api/functions/getOHLC
Actinium.Cloud.define(PLUGIN.ID, 'getOHLC', async (req) => {
const { coinId = 'ethereum', vsCurrency = 'usd', days = '1' } = req.params;
return SDK.getOHLC(coinId, vsCurrency, days);
});
console.log(
'CoinGeckoPlugin registered with Cloud Function on /api/functions/getOHLC'
);
};
export default MOD();SDK: /api/src/app/coingecko/sdk.js
import CoinGeckoService from './coingecko-service.js';
export default {
getOHLC: CoinGeckoService.getOHLC.bind(CoinGeckoService),
};Service: /api/src/app/coingecko/coingecko-service.js
class CoinGeckoService {
async getOHLC(coinId, vsCurrency, days) {
const url = `https://api.coingecko.com/api/v3/coins/${coinId}/ohlc`;
const params = new URLSearchParams({
vs_currency: vsCurrency,
days: days,
});
const response = await fetch(`${url}?${params}`);
const data = await response.json();
return data;
}
}
export default new CoinGeckoService();Actinium auto-discovers and registers Express middleware from plugin files.
From: lib/middleware.js
Middleware files are discovered via glob patterns:
await Promise.all(
globby(ENV.GLOB_MIDDLEWARE) // e.g., 'src/app/**/middleware.js'
.filter((file) => isMiddleware(fs.readFileSync(file, 'utf8')))
.map((file) => import(normalizeImportPath(file)))
);A file is considered middleware if it contains:
Actinium.Middleware.registerActinium.Middleware.unregister
Pattern: middleware/my-middleware/middleware.js or src/app/my-plugin/middleware.js
import Actinium from '@atomic-reactor/actinium-core';
Actinium.Middleware.register(
'my-middleware', // Middleware ID
(app) => {
// Callback receives Express app
app.use((req, res, next) => {
// Express middleware logic
console.log('Request:', req.method, req.url);
next();
});
},
Actinium.Enums.priority.neutral, // Priority
'unique-middleware-id' // Optional unique ID
);Middleware is registered in priority order (lowest priority number first). This is crucial for:
- Body parsers need to run early
- CORS should be before routes
- Authentication before protected routes
- Error handlers should be last
Actinium includes several core middleware modules:
From: /api/actinium_modules/@atomic-reactor/actinium-core/middleware/
- body_parser: JSON and URL-encoded body parsing
- cookie_parser: Cookie parsing
- cookie_session: Session management
- cors: Cross-origin resource sharing
- morgan: HTTP request logging
- parse: Parse Server mounting
- static: Static file serving
- docs: API documentation serving
From: /api/actinium_modules/@atomic-reactor/actinium-core/middleware/cors/middleware.js
import Actinium from '@atomic-reactor/actinium-core';
import cors from 'cors';
Actinium.Middleware.register(
'cors',
(app) => {
app.use(cors());
},
Actinium.Enums.priority.highest,
'ACTINIUM-MIDDLEWARE-CORS'
);// src/app/my-plugin/middleware.js
import Actinium from '@atomic-reactor/actinium-core';
Actinium.Middleware.register(
'request-logger',
(app) => {
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
},
Actinium.Enums.priority.high, // Run early
'my-plugin-logger'
);
// Add custom routes
Actinium.Middleware.register(
'custom-routes',
(app) => {
app.get('/api/custom-endpoint', (req, res) => {
res.json({ message: 'Custom endpoint response' });
});
},
Actinium.Enums.priority.neutral,
'my-plugin-routes'
);Actinium.Middleware.unregister('my-middleware-id');Actinium is built on Parse Server, providing full access to Parse features.
From: middleware/parse/middleware.js
Parse Server is mounted as Express middleware:
const parseConfig = {
appId: ENV.APP_ID,
masterKey: ENV.MASTER_KEY,
serverURL: ENV.SERVER_URL,
databaseURI: ENV.DATABASE_URI,
fileKey: ENV.FILE_KEY,
javascriptKey: ENV.JAVASCRIPT_KEY,
// ... more configuration
};
const parseServer = new ParseServer(parseConfig);
app.use(ENV.PARSE_MOUNT, parseServer.app);Default mount point: /parse (configurable via ENV.PARSE_MOUNT)
Parse Server uses MongoDB for data storage:
// ENV.DATABASE_URI example:
// 'mongodb://localhost:27017/actinium'Parse Collections (MongoDB collections):
_User: User accounts_Role: Roles_Session: User sessions_Installation: Device installations- Custom classes: Any other data models
Create and save:
const MyClass = Actinium.Object.extend('MyClass');
const obj = new MyClass();
obj.set('fieldName', 'value');
obj.set('numberField', 123);
await obj.save(null, { useMasterKey: true });Query:
const query = new Actinium.Query('MyClass');
query.equalTo('fieldName', 'value');
query.limit(10);
const results = await query.find({ useMasterKey: true });Update:
const query = new Actinium.Query('MyClass');
const obj = await query.first({ useMasterKey: true });
obj.set('fieldName', 'newValue');
await obj.save(null, { useMasterKey: true });Delete:
await obj.destroy({ useMasterKey: true });Parse provides granular permission control:
const acl = new Actinium.ACL();
// Public read, no public write
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(false);
// User-specific permissions
acl.setReadAccess(user, true);
acl.setWriteAccess(user, true);
// Role-based permissions
acl.setRoleReadAccess('Admin', true);
acl.setRoleWriteAccess('Admin', true);
obj.setACL(acl);
await obj.save(null, { useMasterKey: true });The master key bypasses all ACLs and permissions:
// Use sparingly and only in trusted backend code
await query.find({ useMasterKey: true });
await obj.save(null, { useMasterKey: true });Security Warning: Never expose master key to clients.
Actinium automatically creates hooks for Parse lifecycle events:
// Before save
Actinium.Hook.register('before-save-MyClass', async (req, context) => {
const { object, user, master } = req;
// Validate or modify object
});
// After save
Actinium.Hook.register('after-save-MyClass', async (req, context) => {
const { object } = req;
// Perform post-save actions
});
// Before find
Actinium.Hook.register('before-find-MyClass', async (req, context) => {
const { query } = req;
// Modify query
});Actinium requires ES module syntax (import/export), not CommonJS (require/module.exports).
Required in /api/package.json:
{
"type": "module"
}This tells Node.js to treat .js files as ES modules.
Always use:
import Actinium from '@atomic-reactor/actinium-core';
import express from 'express';
import path from 'node:path';Never use:
// WRONG - will cause errors
const Actinium = require('@atomic-reactor/actinium-core');Always use:
// Named exports
export const myFunction = () => { ... };
export default MyClass;
// Or
const myFunction = () => { ... };
export { myFunction };
export default MyClass;Never use:
// WRONG
module.exports = MyClass;
exports.myFunction = () => { ... };For conditional or lazy loading:
const module = await import('./my-module.js');Required: Must include .js extension in imports for relative paths:
// CORRECT
import MyClass from './MyClass.js';
import config from './config.js';
// WRONG - will fail
import MyClass from './MyClass';Prefer node: prefix for built-ins:
import path from 'node:path';
import fs from 'node:fs';
import http from 'node:http';ES modules support top-level await:
// Allowed at top level
const data = await fetchData();
Actinium.Hook.register('init', async () => {
// Also allowed in async functions
await initializePlugin();
});mkdir -p api/src/app/my-plugin
cd api/src/app/my-plugin// info.js
const PLUGIN = {
ID: 'MyPlugin',
name: 'My Plugin',
description: 'My custom Actinium plugin',
version: '1.0.0',
pluginDependencies: [],
order: 100,
bundle: [],
meta: {
group: 'Custom',
builtIn: false,
},
};
export default PLUGIN;// sdk.js
class MyPluginSDK {
async doSomething(param) {
console.log('Doing something with:', param);
return { success: true, param };
}
async queryData(filters) {
const query = new Actinium.Query('MyData');
if (filters.name) {
query.equalTo('name', filters.name);
}
return query.find({ useMasterKey: true });
}
}
export default new MyPluginSDK();// plugin.js
import Actinium from '@atomic-reactor/actinium-core';
import PLUGIN from './info.js';
import SDK from './sdk.js';
const MOD = () => {
// Attach SDK to Actinium global
Actinium.MyPlugin = Actinium.MyPlugin || SDK;
// Register plugin
Actinium.Plugin.register(PLUGIN, true);
// Register hooks
Actinium.Hook.register(
'init',
async () => {
console.log('MyPlugin initialized');
// Perform initialization tasks
await initializeDatabase();
},
Actinium.Enums.priority.neutral,
'MyPlugin-init'
);
// Register Cloud Functions
Actinium.Cloud.define(PLUGIN.ID, 'myCloudFunction', async (req) => {
const { param } = req.params;
return SDK.doSomething(param);
});
// Register schemas
Actinium.Hook.register(
'schema-created',
async () => {
await createMySchema();
},
Actinium.Enums.priority.neutral,
'MyPlugin-schema'
);
};
const initializeDatabase = async () => {
const schema = new Actinium.Schema('MyData');
try {
await schema.get({ useMasterKey: true });
} catch (err) {
// Schema doesn't exist, create it
schema.addString('name');
schema.addNumber('value');
schema.addBoolean('active');
await schema.save(null, { useMasterKey: true });
}
};
const createMySchema = async () => {
// Additional schema setup
};
export default MOD(); // Execute immediately// middleware.js
import Actinium from '@atomic-reactor/actinium-core';
Actinium.Middleware.register(
'my-plugin-routes',
(app) => {
// Add custom Express routes
app.get('/api/my-plugin/status', (req, res) => {
res.json({
status: 'active',
version: '1.0.0',
});
});
app.post('/api/my-plugin/action', async (req, res) => {
try {
const result = await Actinium.MyPlugin.doSomething(req.body);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
},
Actinium.Enums.priority.neutral,
'MyPlugin-middleware'
);Start Actinium:
cd api
npm startTest Cloud Function:
curl -X POST http://localhost:9000/api/functions/myCloudFunction \
-H "Content-Type: application/json" \
-H "X-Parse-Application-Id: YOUR_APP_ID" \
-d '{"param": "test value"}'const schema = new Actinium.Schema('MyCollection');
// Field types
schema.addString('name');
schema.addNumber('count');
schema.addBoolean('active');
schema.addDate('createdAt');
schema.addArray('tags');
schema.addObject('metadata');
schema.addPointer('user', '_User'); // Relation to User
schema.addRelation('items', 'Item'); // Many-to-many relation
// Indexes
schema.addIndex('name_index', { name: 1 });
// Save schema
await schema.save(null, { useMasterKey: true });From: lib/collection.js
// Register a collection
Actinium.Collection.register('MyCollection', {
create: true, // Allow creation
retrieve: true, // Allow retrieval
update: true, // Allow updates
delete: true, // Allow deletion
});
// List registered collections
const collections = Actinium.Collection.list();// Create a role
const roleACL = new Actinium.ACL();
roleACL.setPublicReadAccess(true);
const role = new Actinium.Role('Editor', roleACL);
await role.save(null, { useMasterKey: true });
// Add user to role
const user = await new Actinium.Query('_User')
.equalTo('username', 'john')
.first({ useMasterKey: true });
role.getUsers().add(user);
await role.save(null, { useMasterKey: true });
// Check if user has role
const hasRole = await Actinium.Roles.User.is(user, 'Editor');// Register capability
Actinium.Capability.register('myPlugin.create', {
allowed: ['Editor', 'Admin'],
excluded: ['Banned'],
});
// Check user capability
const canCreate = await Actinium.Capability.User.can(user, 'myPlugin.create');
if (!canCreate) {
throw new Error('Permission denied');
}From: lib/plugable.js
Plugins define their own capabilities:
// In plugin.js
PLUGIN.capabilities = [
{
capability: 'MyPlugin.create',
roles: {
allowed: ['Admin'],
},
},
{
capability: 'MyPlugin.retrieve',
roles: {
allowed: ['anonymous'], // Public access
},
},
];// GOOD
Actinium.Cloud.define(PLUGIN.ID, 'myFunction', callback);
// BAD - no plugin gating
Parse.Cloud.define('myFunction', callback);// GOOD
const MOD = () => {
// Plugin registration
};
export default MOD(); // Execute immediately
// BAD - function not executed
export default MOD;// Backend code can use master key
await query.find({ useMasterKey: true });
await obj.save(null, { useMasterKey: true });// Makes SDK available everywhere
Actinium.MyPlugin = Actinium.MyPlugin || SDK;// In plugin A
Actinium.Hook.register('data-updated', async (data) => {
// Handle data update
});
// In plugin B
await Actinium.Hook.run('data-updated', newData);Actinium.Hook.register(
'schema-created',
async () => {
const schema = new Actinium.Schema('MyClass');
// Define schema
await schema.save(null, { useMasterKey: true });
},
Actinium.Enums.priority.neutral
);// In .env
MY_API_KEY = secret_key_here;
// In plugin
const apiKey = process.env.MY_API_KEY || ENV.MY_API_KEY;// WRONG - will fail
const Actinium = require('@atomic-reactor/actinium-core');
// CORRECT
import Actinium from '@atomic-reactor/actinium-core';// WRONG
import SDK from './sdk';
// CORRECT
import SDK from './sdk.js';Plugins load in order specified by the order property in info.js. If Plugin B depends on Plugin A, ensure A.order < B.order.
// Hooks run asynchronously, must await
await Actinium.Hook.run('my-hook', data);
// DON'T forget await
Actinium.Hook.run('my-hook', data); // Hook may not complete// CORRECT
Actinium.Cloud.define(PLUGIN.ID, 'myFunc', async (req) => {
const { param1 } = req.params; // Client params are in req.params
});
// WRONG
Actinium.Cloud.define(PLUGIN.ID, 'myFunc', async (param1) => {
// This won't work
});// MUST await Parse queries
const results = await query.find({ useMasterKey: true });
// WRONG
const results = query.find({ useMasterKey: true }); // Returns PromiseUsing { useMasterKey: true } bypasses all ACLs. Use carefully.
Actinium is a global object, available without import in plugin files:
// No need to import in plugin.js
// Actinium is already global
Actinium.Plugin.register(PLUGIN, true);But for type safety and clarity, you can still import:
import Actinium from '@atomic-reactor/actinium-core';Middleware must be registered before Actinium.init() completes. Don't register middleware in late hooks.
After modifying Parse schemas, restart the server for changes to take effect.
Complete example from this project:
info.js:
const PLUGIN = {
ID: 'CoinGeckoPlugin',
name: 'Coinbase Plugin',
description: 'CoinGecko API integration for crypto market data.',
version: '1.0.0',
pluginDependencies: [],
order: 100,
bundle: [],
meta: {
group: 'CoinGecko',
builtIn: false,
},
};
export default PLUGIN;plugin.js:
import Actinium from '@atomic-reactor/actinium-core';
import PLUGIN from './info.js';
import SDK from './sdk.js';
const MOD = () => {
Actinium.CoinGecko = Actinium.CoinGecko || SDK;
Actinium.Plugin.register(PLUGIN, true);
Actinium.Cloud.define(PLUGIN.ID, 'getOHLC', async (req) => {
const { coinId = 'ethereum', vsCurrency = 'usd', days = '1' } = req.params;
return SDK.getOHLC(coinId, vsCurrency, days);
});
console.log('CoinGeckoPlugin registered');
};
export default MOD();sdk.js:
import CoinGeckoService from './coingecko-service.js';
export default {
getOHLC: CoinGeckoService.getOHLC.bind(CoinGeckoService),
};coingecko-service.js:
class CoinGeckoService {
async getOHLC(coinId, vsCurrency, days) {
const url = `https://api.coingecko.com/api/v3/coins/${coinId}/ohlc`;
const params = new URLSearchParams({
vs_currency: vsCurrency,
days: days,
});
const response = await fetch(`${url}?${params}`);
return response.json();
}
}
export default new CoinGeckoService();Usage from frontend:
const data = await Parse.Cloud.run('getOHLC', {
coinId: 'bitcoin',
vsCurrency: 'usd',
days: '7',
});Actinium is a powerful backend framework that provides:
- Parse Server Foundation: Full MongoDB integration, user management, ACLs
- Plugin Architecture: Auto-discoverable, modular plugins
- Hook System: Event-driven extensibility at every level
- Cloud Function Gateway: Plugin-based Cloud Functions with permission gating
- Middleware System: Convention-based Express middleware
- ES Module First: Modern JavaScript module system
- Capability-Based Permissions: Fine-grained access control
- Extensible Global Object:
Actiniumglobal with all framework features
By leveraging Actinium's conventions and features, you can build scalable, maintainable backend systems with robust plugin ecosystems.
For frontend integration with Reactium, see REACTIUM_FRAMEWORK.md.