Purpose: Define Parse Server collections with schema, Class-Level Permissions (CLP), indexes, and capability mappings
Source: Actinium-Plugins/actinium_modules/@atomic-reactor/actinium-core/lib/collection.js:1-426
Collection registration is the core data modeling and security configuration system in Actinium. Every Parse Server collection must be registered to:
- Define schema - Field types, constraints, relationships
- Set Class-Level Permissions (CLP) - Role-based access control at collection level
- Create indexes - Query optimization for high-entropy fields
- Map capabilities - Link Actinium capabilities to Parse permissions
- Enable schema evolution - Add/modify/delete fields safely
Key Insight: Collections registered with Actinium.Collection.register() automatically get capability-based security, with CLPs dynamically generated from role capabilities.
Source: actinium-core/lib/collection.js:22-66
Actinium.Collection.register(
collection, // String: Collection name
publicSetting, // Object: Public permission flags
schema, // Object: Parse field definitions (optional)
indexes // Array: Field names to index (optional)
)collection (String, required)
Parse Server collection name (e.g., 'Setting', 'Content', '_User', '_Role').
publicSetting (Object, required)
Flags indicating which operations are publicly accessible:
{
create: false, // Can public create new objects?
retrieve: false, // Can public read objects?
update: false, // Can public modify objects?
delete: false, // Can public delete objects?
addField: false // Can public add new fields?
}true= Public access (maps to'*': truein CLP)false= Role-restricted access (requires capability)
Default: All false (fully private, admin-only)
schema (Object, optional)
Parse Server field definitions:
{
fieldName: {
type: 'String', // Required: Parse type
required: false, // Optional: Field required?
defaultValue: null, // Optional: Default value
delete: false // Optional: Mark for deletion
}
}Supported Parse Types:
- Primitives:
'String','Number','Boolean','Date','Array','Object' - Parse-specific:
'Pointer','Relation','File','GeoPoint','Polygon' - Pointers/Relations require
targetClass: 'CollectionName'
indexes (Array, optional)
List of field names to index for query performance:
['key', 'uuid', 'slug']Source: actinium-settings/plugin.js:284-302
const COLLECTION = 'Setting';
// Register capabilities first
Actinium.Capability.register(`${COLLECTION}.create`, {}, Actinium.Enums.priority.highest);
Actinium.Capability.register(`${COLLECTION}.retrieve`, {}, Actinium.Enums.priority.highest);
Actinium.Capability.register(`${COLLECTION}.update`, {}, Actinium.Enums.priority.highest);
Actinium.Capability.register(`${COLLECTION}.delete`, {}, Actinium.Enums.priority.highest);
Actinium.Capability.register(`${COLLECTION}.addField`, {}, Actinium.Enums.priority.highest);
// Register collection with schema
Actinium.Collection.register(
COLLECTION,
{
create: false, // Admin-only
retrieve: false, // Admin-only
update: false, // Admin-only
delete: false, // Admin-only
addField: false // Admin-only
},
{
key: { type: 'String' },
value: { type: 'Object' }
},
['key'] // Index the 'key' field
);Source: actinium-roles/plugin.js:40-46
Actinium.Collection.register('_Role', {
create: false,
retrieve: true, // ✓ Anyone can read roles
update: false,
delete: false,
addField: false
});Source: actinium-content/schema.js:1-56
Actinium.Collection.register(
'Content',
{
create: true, // Public can create
retrieve: true, // Public can read
update: true, // Public can update
delete: true, // Public can delete
addField: false // Only admins add fields
},
{
title: { type: 'String' },
slug: { type: 'String' },
uuid: { type: 'String' },
meta: { type: 'Object' },
data: { type: 'Object' },
status: { type: 'String' },
// Pointer to Type collection
type: {
type: 'Pointer',
targetClass: 'Type'
},
// Pointer to User
user: {
type: 'Pointer',
targetClass: '_User'
},
// Self-referential pointer
parent: {
type: 'Pointer',
targetClass: 'Content'
},
// Relation to self
children: {
type: 'Relation',
targetClass: 'Content'
},
// Relation to Taxonomy
taxonomy: {
type: 'Relation',
targetClass: 'Taxonomy'
},
file: { type: 'File' }
},
['uuid', 'slug', 'title'] // Index multiple fields
);Source: actinium-type/plugin.js:102-180
const COLLECTION = 'Type';
// Grant capabilities to specific roles
Actinium.Capability.register(`${COLLECTION}.create`, {
allowed: ['contributor', 'moderator']
}, Actinium.Enums.priority.highest);
Actinium.Capability.register(`${COLLECTION}.retrieve`, {
allowed: ['anonymous', 'contributor', 'moderator', 'user']
}, Actinium.Enums.priority.highest);
Actinium.Capability.register(`${COLLECTION}.update`, {
allowed: ['moderator', 'contributor']
}, Actinium.Enums.priority.highest);
Actinium.Capability.register(`${COLLECTION}.delete`, {
allowed: ['moderator', 'contributor']
}, Actinium.Enums.priority.highest);
Actinium.Capability.register(`${COLLECTION}.addField`, {}, Actinium.Enums.priority.highest);
// Register with public retrieve enabled
Actinium.Collection.register(
COLLECTION,
{
create: false,
retrieve: true, // Public + role capabilities
update: false,
delete: false,
addField: false
},
{
uuid: { type: 'String' },
type: { type: 'String' },
collection: { type: 'String' },
machineName: { type: 'String' },
namespace: { type: 'String' },
fields: { type: 'Object' },
meta: { type: 'Object' },
slugs: { type: 'Array' }
},
['uuid', 'machineName', 'collection']
);How Actinium maps capabilities to Parse Server CLPs:
Source: actinium-core/lib/collection.js:135-177
For each operation (create, retrieve, update, delete, addField):
-
Get capability name:
${collection}.${operation}(lowercase)- Example:
Setting.retrieve,Content.create
- Example:
-
Fetch allowed roles from capability:
const allowed = op.get(currentCap, 'allowed', []); // Example: ['contributor', 'moderator']
-
Build CLP entry:
{ [capabilityName]: { 'role:contributor': true, 'role:moderator': true, 'role:administrator': true, // Always added 'role:super-admin': true // Always added } }
-
Check public setting:
if (publicSetting[operation] === true || allowed.includes('anonymous')) { CLP[capabilityName] = { '*': true }; // Override with public access }
Source: actinium-core/lib/collection.js:180-224
| Capability | Parse Permissions |
|---|---|
create |
create |
retrieve |
find, count, get |
update |
update |
delete |
delete |
addField |
addField |
Example CLP Output:
{
classLevelPermissions: {
find: { 'role:contributor': true, 'role:administrator': true, 'role:super-admin': true },
get: { 'role:contributor': true, 'role:administrator': true, 'role:super-admin': true },
count: { 'role:contributor': true, 'role:administrator': true, 'role:super-admin': true },
create: { 'role:moderator': true, 'role:administrator': true, 'role:super-admin': true },
update: { 'role:administrator': true, 'role:super-admin': true },
delete: { 'role:administrator': true, 'role:super-admin': true },
addField: { 'role:administrator': true, 'role:super-admin': true }
}
}Fires before CLP generation for each collection.
Source: actinium-core/lib/collection.js:109-114
Actinium.Hook.register('collection-before-permissions', async (collection, publicSetting) => {
// Modify publicSetting before CLP generation
if (collection === 'MyCollection') {
publicSetting.retrieve = true;
}
});Modify CLPs after generation, before applying to schema.
Source: actinium-core/lib/collection.js:273
Actinium.Hook.register('collection-clp', async ({ collection, CLP }) => {
if (collection === '_User') {
// Custom CLP modifications
Object.keys(CLP).forEach(key => {
CLP[key]['role:custom-role'] = true;
});
}
});Real-World Usage: actinium-users/plugin.js:305-314
Modify indexes before applying to schema.
Source: actinium-core/lib/collection.js:275-278
Actinium.Hook.register('collection-indexes', async ({ collection, newIndexes }) => {
if (collection === 'Content') {
// Add compound index
newIndexes['status_createdAt'] = { status: 1, createdAt: -1 };
}
});Fires before schema/CLP loading begins.
Source: actinium-core/lib/collection.js:82
Actinium.Hook.register('collection-before-load', async (collection) => {
console.log(`Loading schema for ${collection || 'all collections'}`);
});Define fields in schema object when registering:
Actinium.Collection.register('MyCollection', actions, {
newField: {
type: 'String',
required: true,
defaultValue: 'default'
}
});On next startup: Field added to Parse Server schema automatically.
Actinium.Collection.register('MyCollection', actions, {
oldField: {
type: 'String',
delete: true // ← Marks field for deletion
}
});Source: actinium-core/lib/collection.js:252-268
How it works:
- Sets
__op: 'Delete'on field - Parse Server removes field from schema
- Existing data remains but field is inaccessible
Workaround:
- Create new field with different name
- Migrate data via cloud function
- Delete old field
- Rename new field (requires Parse Dashboard)
Source: actinium-core/lib/collection.js:38-59
When a capability is modified (role added/removed), CLPs automatically reload:
Actinium.Hook.register('capability-change', async (req) => {
const capability = req.object.get('group');
// Check if capability affects this collection
if ([`${collection}.create`, `${collection}.retrieve`, ...].includes(capability)) {
await Actinium.Collection.load(collection); // ← Reload CLPs
}
});Example Flow:
- Admin grants
Content.createtocontributorrole capability-changehook firesCollection.load('Content')re-generates CLPs- New CLP applied to Parse Server schema
- Contributors can now create Content
Source: actinium-core/lib/collection.js:81-326
- Plugin registers collection during
initorschemahook - Capabilities registered (before collection registration)
- Collection registered with schema/indexes/actions
- On
Collection.load()(fires during startup):collection-before-loadhook- For each collection:
collection-before-permissionshook- Fetch existing Parse schema (or create empty)
- Generate CLPs from capabilities
collection-clphook (modify CLPs)collection-indexeshook (modify indexes)- Apply schema via Parse SchemaController
- Collection.loaded = true
Source: actinium-core/lib/collection.js:281-305
// Update existing schema
if (className) {
schemaController.update(collection, {
className: collection,
classLevelPermissions: CLP,
fields,
indexes: newIndexes
}, Actinium.Utils.MasterOptions());
}
// Create new schema
else {
schemaController.create(collection, {
className: collection,
classLevelPermissions: CLP,
fields,
indexes: newIndexes
}, Actinium.Utils.MasterOptions());
}Source: actinium-settings/plugin.js:284-302
Actinium.Collection.register(
'Setting',
{ create: false, retrieve: false, update: false, delete: false, addField: false },
{
key: { type: 'String' },
value: { type: 'Object' }
},
['key']
);Result:
- Only
administratorandsuper-admincan access keyfield indexed for fast lookups- Object-path stored in
valuefield
Source: actinium-type/plugin.js:144-180
// Contributors can CRUD, anyone can read
Actinium.Capability.register('Type.create', { allowed: ['contributor', 'moderator'] });
Actinium.Capability.register('Type.retrieve', { allowed: ['anonymous', 'contributor', 'moderator', 'user'] });
Actinium.Capability.register('Type.update', { allowed: ['moderator', 'contributor'] });
Actinium.Capability.register('Type.delete', { allowed: ['moderator', 'contributor'] });
Actinium.Collection.register(
'Type',
{ create: false, retrieve: true, update: false, delete: false, addField: false },
{
uuid: { type: 'String' },
machineName: { type: 'String' },
fields: { type: 'Object' },
// ... more fields
},
['uuid', 'machineName', 'collection']
);Result:
- Anyone can read types (retrieve: true + anonymous in allowed)
- Contributors/moderators can create/update/delete
- Three fields indexed for queries
Source: actinium-content/schema.js:1-56
{
collection: 'Content',
actions: {
create: true,
retrieve: true,
update: true,
delete: true,
addField: false
},
schema: {
title: { type: 'String' },
slug: { type: 'String' },
type: { type: 'Pointer', targetClass: 'Type' },
user: { type: 'Pointer', targetClass: '_User' },
taxonomy: { type: 'Relation', targetClass: 'Taxonomy' }
},
indexes: ['uuid', 'slug', 'title']
}Result:
- Fully public CRUD (all operations have
'*': true) - Complex relational schema
- Multiple indexes for content queries
// ✓ CORRECT ORDER
Actinium.Capability.register('MyCollection.create', { allowed: ['contributor'] });
Actinium.Collection.register('MyCollection', { create: false, ... });
// ✗ WRONG ORDER - Collection registration won't find capability
Actinium.Collection.register('MyCollection', { create: false, ... });
Actinium.Capability.register('MyCollection.create', { allowed: ['contributor'] });Actinium.Capability.register(
'Setting.create',
{},
Actinium.Enums.priority.highest // ← Ensures capability exists before CLP generation
);// ✓ GOOD - High-entropy field used in queries
Actinium.Collection.register('Content', actions, schema, ['uuid']);
// ✗ BAD - Low-entropy field (few unique values)
Actinium.Collection.register('Content', actions, schema, ['status']);
// ✗ BAD - Over-indexing
Actinium.Collection.register('Content', actions, schema, [
'title', 'slug', 'uuid', 'status', 'createdAt', 'updatedAt' // Too many!
]);Rule of Thumb: Index only fields used in query.equalTo() or query.contains() frequently.
Actinium.Hook.register('schema', async () => {
if (!Actinium.Plugin.isActive('MyPlugin')) return;
Actinium.Collection.register('MyCollection', actions, schema, indexes);
});Why: schema hook fires after capabilities loaded, before collection loading.
// schema.js
export default [
{
collection: 'Content',
actions: { create: true, retrieve: true, ... },
schema: { title: { type: 'String' }, ... },
indexes: ['uuid', 'slug']
}
];
// plugin.js
import PLUGIN_SCHEMA from './schema.js';
Actinium.Hook.register('schema', async () => {
PLUGIN_SCHEMA.forEach(({ actions, collection, schema, indexes }) => {
Actinium.Collection.register(collection, actions, schema, indexes);
});
});Source: actinium-content/plugin.js:68-81
// This does NOT grant public access!
Actinium.Collection.register('MyCollection', {
retrieve: false // ← Still requires capability
});Fix: Set retrieve: true OR add anonymous to capability:
Actinium.Capability.register('MyCollection.retrieve', { allowed: ['anonymous'] });// Initial registration
Actinium.Collection.register('MyCollection', actions, {
count: { type: 'String' } // Oops, should be Number
});
// Later (DOES NOT WORK!)
Actinium.Collection.register('MyCollection', actions, {
count: { type: 'Number' } // ← Parse Server error: Cannot change type
});Fix: Migrate data to new field, delete old.
// Collection registered, but capability never defined
Actinium.Collection.register('MyCollection', { create: false, ... });
// Cloud function fails: "Permission denied"
Actinium.Cloud.run('mycollection-create', params);Fix: Always register capabilities for operations:
Actinium.Capability.register('MyCollection.create', { allowed: ['contributor'] });// Over-indexed collection
Actinium.Collection.register('Content', actions, schema, [
'title', 'slug', 'status', 'createdAt', 'updatedAt', 'user', 'type'
]);Impact:
- Every write rebuilds 7 indexes
- Slows down content creation/updates
- Most indexes never used in queries
Fix: Index only high-value, frequently-queried fields.
// Trying to lock out administrators (IMPOSSIBLE)
Actinium.Hook.register('collection-clp', async ({ collection, CLP }) => {
if (collection === 'Secret') {
delete CLP.find['role:administrator']; // ← Has no effect!
}
});Source: actinium-core/lib/collection.js:167-176
Why: Collection registration explicitly adds administrator/super-admin to every operation.
Workaround: Use object-level ACLs instead of CLPs.
After modifying schema in code:
Actinium.Collection.register('MyCollection', actions, {
newField: { type: 'String' } // ← Added
});Schema won't update until:
- Server restart, OR
- Manual
Actinium.Collection.load('MyCollection')call
In Production: Use migration cloud functions instead of in-place schema changes.
Source: Actinium Capabilities Deep Dive
Collection registration relies on capabilities:
// Capability defines who can perform operation
Actinium.Capability.register('Content.create', { allowed: ['contributor'] });
// Collection references capability in CLP generation
Actinium.Collection.register('Content', { create: false, ... });Result: role:contributor added to create CLP.
Source: Cloud Function Patterns
Collections define available operations for cloud functions:
// Collection allows public retrieve
Actinium.Collection.register('Setting', { retrieve: false, ... });
// Cloud function respects CLP
Actinium.Cloud.define('Settings', 'setting-get', async (req) => {
// Automatically enforces Setting.retrieve capability
return Actinium.Setting.get(req.params.key, null, CloudRunOptions(req));
});Actinium uses Parse SchemaController directly:
const schemaController = Parse.CoreManager.getSchemaController();
await schemaController.update(collection, schemaDefinition, MasterOptions());Bypass Actinium (advanced):
// Direct Parse schema modification (use with caution!)
const schema = new Parse.Schema('MyCollection');
schema.addString('newField');
await schema.save({ useMasterKey: true });| Feature | Actinium Collection | Direct Parse Schema |
|---|---|---|
| Capability integration | ✓ Automatic | Manual CLP config |
| Role-based security | ✓ Dynamic from caps | Static CLP |
| Hook extensibility | ✓ 4 hooks | None |
| Index management | ✓ Declarative | Imperative |
| Schema evolution | ✓ Version-tracked | Manual |
| Public/private toggle | ✓ Single flag | CLP object |
| Auto-reload on cap change | ✓ Yes | No |
When to use Collection.register:
- Plugin-managed collections
- Role-based access control needed
- Schema changes via code updates
- Need hook-based customization
When to use Parse Schema directly:
- One-time schema setup
- Non-role-based security
- Low-level schema manipulation
- Testing/migration scripts
interface PublicSetting {
create: boolean;
retrieve: boolean;
update: boolean;
delete: boolean;
addField: boolean;
}
interface SchemaField {
type: 'String' | 'Number' | 'Boolean' | 'Date' | 'Array' | 'Object' |
'Pointer' | 'Relation' | 'File' | 'GeoPoint' | 'Polygon';
targetClass?: string; // Required for Pointer/Relation
required?: boolean;
defaultValue?: any;
delete?: boolean;
}
interface CollectionSchema {
[fieldName: string]: SchemaField;
}
function register(
collection: string,
publicSetting: PublicSetting,
schema?: CollectionSchema,
indexes?: string[]
): Promise<void>;import Actinium from '@atomic-reactor/actinium-sdk-core/server';
const COLLECTION = 'Article';
const schema: CollectionSchema = {
title: { type: 'String', required: true },
slug: { type: 'String' },
author: { type: 'Pointer', targetClass: '_User' },
tags: { type: 'Relation', targetClass: 'Tag' },
publishedAt: { type: 'Date' }
};
const publicSettings: PublicSetting = {
create: false,
retrieve: true,
update: false,
delete: false,
addField: false
};
Actinium.Collection.register(COLLECTION, publicSettings, schema, ['slug']);// plugin.js (version 1.0.0 → 1.1.0)
Actinium.Hook.register('schema', async () => {
if (!Actinium.Plugin.isActive('MyPlugin')) return;
// New collection in v1.1.0
Actinium.Capability.register('NewCollection.create', { allowed: ['contributor'] });
Actinium.Capability.register('NewCollection.retrieve', { allowed: ['anonymous'] });
Actinium.Collection.register(
'NewCollection',
{ create: false, retrieve: true, update: false, delete: false, addField: false },
{
name: { type: 'String' },
data: { type: 'Object' }
},
['name']
);
});On plugin update: New collection schema created automatically on next server start.
// Old schema (v1.0.0)
Actinium.Collection.register('Article', actions, {
authorName: { type: 'String' } // ← Migrating away from this
});
// New schema (v2.0.0)
Actinium.Collection.register('Article', actions, {
authorName: { type: 'String', delete: true }, // Mark for deletion
author: { type: 'Pointer', targetClass: '_User' } // New field
});
// Migration cloud function
Actinium.Cloud.define('MyPlugin', 'migrate-article-authors', async (req) => {
const articles = await new Actinium.Query('Article')
.limit(1000)
.find(Actinium.Utils.MasterOptions());
for (const article of articles) {
const authorName = article.get('authorName');
const user = await new Actinium.Query('_User')
.equalTo('username', authorName)
.first(Actinium.Utils.MasterOptions());
if (user) {
article.set('author', user);
await article.save(null, Actinium.Utils.MasterOptions());
}
}
return { migrated: articles.length };
});Collection registration is Actinium's declarative data modeling system:
- Single registration defines schema, CLPs, indexes, and capabilities
- Automatic CLP generation from role capabilities
- Dynamic security - CLPs update when capabilities change
- Hook-extensible - Customize CLPs and indexes before application
- Schema evolution - Add/modify/delete fields via code updates
Key Files:
- Implementation:
actinium-core/lib/collection.js:1-426 - Real examples:
actinium-settings/plugin.js,actinium-type/plugin.js,actinium-content/schema.js
Next Steps:
- Cloud Functions - Using collections in APIs
- Capability System - Understanding capability-based security
- Schema Initialization Pattern - Best practices