The SPID (Service Provider ID) system provides multi-tenancy in FOAM3 by isolating data and permissions per service provider. This enables hosting multiple independent tenants in a single application instance with guaranteed data isolation.
┌─────────────────────────────────────────────────────────────────────────────┐
│ SPID SYSTEM ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────┐ implements ┌─────────────────────────┐
│ MyModel │ ───────────────────────────►│ ServiceProviderAware │
└──────────┘ │ (interface) │
│ - spid property │
│ - GLOBAL_SPID = "*" │
└─────────────────────────┘
│
│ persisted in
▼
┌──────────────────────────────────────────────────────────────────┐
│ ServiceProviderAwareDAO │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Automatic SPID Filtering: │ │
│ │ • put_() → sets spid on create, checks permissions │ │
│ │ • find_() → filters by serviceprovider.read.<spid> │ │
│ │ • select_() → adds SPID predicate to all queries │ │
│ │ • remove_() → filters by serviceprovider.read.<spid> │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
┌──────────┐ belongs to ┌─────────────────────────┐
│ User │ ───────────────────────────►│ ServiceProvider │
│ (spid) │ │ (Capability) │
└──────────┘ │ - inherentPermissions │
│ - setupSpid() │
│ - removeSpid() │
└─────────────────────────┘
To make a class SPID-aware, implement the ServiceProviderAware interface:
foam.CLASS({
package: 'com.example',
name: 'MyModel',
implements: [
'foam.core.auth.ServiceProviderAware'
],
properties: [
// The 'spid' property is inherited automatically from the interface
{ class: 'String', name: 'name' },
{ class: 'Long', name: 'amount' }
// ... your other properties
]
});
That’s all you need. The interface provides:
spid property (Reference to foam.core.auth.ServiceProvider)GLOBAL_SPID constant (*) for wildcard accessisGlobalSpid() method to check for global SPID (Java only)The spid property is defined with externalTransient: true:
Location: foam3/src/foam/core/auth/ServiceProviderAware.js
foam.INTERFACE({
package: 'foam.core.auth',
name: 'ServiceProviderAware',
constants: [
{
name: 'GLOBAL_SPID',
value: '*',
type: 'String'
}
],
properties: [
{
class: 'Reference',
of: 'foam.core.auth.ServiceProvider',
name: 'spid',
externalTransient: true
}
]
});
Location: foam3/src/foam/core/auth/ServiceProvider.js
ServiceProvider extends Capability and represents a tenant:
foam.CLASS({
package: 'foam.core.auth',
name: 'ServiceProvider',
extends: 'foam.core.crunch.Capability',
properties: [
{
class: 'String',
name: 'id',
// Must be lowercase alphanumeric with dots/hyphens
// e.g., "acme", "acme.corp", "my-tenant"
},
{
name: 'inherentPermissions',
// Automatically grants these permissions:
// - serviceprovider.read.<spid>
// - serviceproviderdao.read.<spid>
},
{
class: 'Long',
name: 'anonymousUser'
// Reference to anonymous user for this SPID
}
],
methods: [
// setupSpid(x, user) - Grants SPID capabilities to user
// removeSpid(x, user) - Removes SPID capabilities from user
]
});
Location: foam3/src/foam/core/auth/ServiceProviderAwareDAO.js
A DAO decorator that enforces SPID-based access control:
| Operation | Behavior |
|---|---|
put_() (create) |
Sets SPID from context if not set; checks serviceprovider.read.<spid> |
put_() (update) |
Checks serviceprovider.read.<spid>; changing SPID requires serviceprovider.update.<oldSpid> and serviceprovider.update.<newSpid> |
find_() |
Filters by SPIDs user has serviceprovider.read.<spid> permission for |
select_() |
Adds SPID predicate to all queries automatically |
remove_() |
Allowed with serviceprovider.remove.*, or when the object’s SPID is one the user has serviceprovider.read.<spid> for |
| Permission | Purpose |
|---|---|
serviceprovider.read.<spid> |
Read objects in this SPID |
serviceprovider.update.<spid> |
Modify SPID assignment of objects |
serviceprovider.read.* |
Global read access (bypasses all SPID filtering) |
serviceprovider.update.* |
Global update access |
serviceprovider.remove.* |
Global remove access |
When a ServiceProvider is created, it automatically grants:
serviceprovider.read.<spid>serviceproviderdao.read.<spid>These are defined in the inherentPermissions property.
When creating a new SPID-aware object, the SPID is determined in this order:
x.get("spid")spid, it replaces the context SPIDspid.default.theme permission and an empty spid// From ServiceProviderAwareDAO.getSpid()
User user = null;
String spid = (String) x.get("spid");
Subject subject = (Subject) x.get("subject");
AuthService auth = (AuthService) x.get("auth");
if ( subject != null ) {
user = subject.getUser();
if ( user != null ) {
if ( ! SafetyUtil.isEmpty(user.getSpid()) ) {
spid = user.getSpid();
}
}
}
if ( SafetyUtil.isEmpty(spid) ||
user != null &&
auth.check(x, "spid.default.theme") &&
SafetyUtil.isEmpty(user.getSpid()) ) {
Theme theme = ((Themes) x.get("themes")).findTheme(x);
if ( theme != null &&
! SafetyUtil.isEmpty(theme.getSpid()) ) {
spid = theme.getSpid();
}
}
if ( SafetyUtil.isEmpty(spid) ) {
throw new AuthorizationException();
}
return spid;
EasyDAO automatically adds ServiceProviderAwareDAO when the model implements ServiceProviderAware.
From EasyDAO.js:
{
name: 'serviceProviderAware',
class: 'Boolean',
javaFactory: 'return getOf().isAssignableTo(foam.core.auth.ServiceProviderAware.class);'
}
Standard services.jrl pattern:
p({
"class": "foam.core.boot.CSpec",
"name": "myModelDAO",
"serve": true,
"serviceScript": """
return new foam.dao.EasyDAO.Builder(x)
.setSeqNo(true)
.setOf(com.example.MyModel.getOwnClassInfo())
.build();
"""
})
EasyDAO automatically:
MyModel implements ServiceProviderAwareServiceProviderAwareDAO to the decorator chainspid property for performanceTo disable automatic SPID filtering for a specific DAO:
return new foam.dao.EasyDAO.Builder(x)
.setOf(com.example.MyModel.getOwnClassInfo())
.setServiceProviderAware(false)
.build();
When manually crafting a DAO stack (without EasyDAO):
delegate = new foam.core.auth.ServiceProviderAwareDAO.Builder(x)
.setDelegate(delegate)
.build();
Note: Adding ServiceProviderAwareDAO manually when using EasyDAO results in double decoration since EasyDAO already adds it automatically.
A typical SPID-aware DAO setup (created automatically by EasyDAO):
Request
│
▼
┌──────────────────────────┐
│ AuthorizationDAO │ ← Permission checks
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ ServiceProviderAwareDAO │ ← SPID filtering (auto-added by EasyDAO)
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ MDAO / JDAO │ ← Persistence
└──────────────────────────┘
// User created without SPID
var user = User.create({
email: 'john@example.com',
firstName: 'John'
});
await userDAO.put(user);
// User initially sees data from their theme's SPID
// Admin assigns SPID to user
user.spid = 'acme';
await userDAO.put(user);
// Triggers CreateUserCapabilityJunctionOnSpidSet rule
// Which calls ServiceProvider.setupSpid(x, user)
UserCapabilityJunction recordsGRANTEDserviceprovider.read.acme permission// Admin changes user's SPID
user.spid = 'newcorp';
await userDAO.put(user);
// 1. removeSpid() called for old SPID ('acme')
// 2. setupSpid() called for new SPID ('newcorp')
// User can now only see 'newcorp' data
The SPID can be accessed and manipulated in the execution context:
// Get current SPID from context
String spid = (String) x.get("spid");
// Set SPID in context (for system operations)
X newContext = x.put("spid", "acme");
// Clear SPID (used during login)
X loginContext = x.put("spid", null);
During login, the SPID is cleared to prevent SPID leakage:
public class ResetSpidBeforeLoginAuthService {
public User login(X x, String identifier, String password) {
// Clear SPID during authentication
return getDelegate().login(x.put("spid", null), identifier, password);
}
}
The special SPID value * represents global/shared data:
// Check if object has global SPID (in Java: myObject.isGlobalSpid())
if ( myObject.spid === '*' ) {
// Object is accessible to all tenants
}
// Set global SPID
myObject.spid = '*'; // ServiceProviderAware.GLOBAL_SPID
Objects with global SPID are visible to all users regardless of their assigned SPID.
Classes that implement ServiceProviderAware in FOAM3:
User - Each user belongs to a specific SPIDTheme - Themes are scoped per SPIDTicket - Support tickets isolated by SPIDFlow - Business flows per tenantRule - Business rules can be SPID-specificUsed in rules/queries to check user’s SPID:
foam.CLASS({
package: 'foam.core.crunch.predicate',
name: 'IsSpid',
properties: [
{
class: 'StringArray',
name: 'spids'
}
]
});
// Usage in a rule
{
predicate: {
class: 'foam.core.crunch.predicate.IsSpid',
spids: ['acme', 'newcorp']
}
}
Wraps predicates to add SPID filtering for related objects:
foam.CLASS({
package: 'foam.core.auth',
name: 'ServiceProviderAwarePredicate',
extends: 'foam.mlang.predicate.AbstractPredicate'
});
ServiceProviderAwareDAO*) is for shared data accessible to all tenantsCommon issues and solutions:
| Issue | Cause | Solution |
|---|---|---|
| AuthorizationException on create | No SPID in context | User needs SPID assigned, or theme needs SPID configured |
| User can’t see data | Missing serviceprovider.read.<spid> |
Check UserCapabilityJunction for the user |
| Data visible across tenants | Model missing ServiceProviderAware |
Model needs implements: ['foam.core.auth.ServiceProviderAware'] |
| Data visible across tenants (manual DAO) | Missing ServiceProviderAwareDAO | Relevant when manually crafting DAO stack without EasyDAO |
| SPID not set on new objects | Context SPID empty | User SPID or theme SPID needs to be set |