OSGi Configuration
OSGi configuration is the primary mechanism for controlling the behavior of AEM services at runtime. In a modern AEM project, configurations are stored as JSON files inside the code repository and deployed alongside the application — ensuring that every environment receives consistent, version-controlled settings.
Storing OSGi configs in the repository means they are reviewed in pull requests, tracked in Git history, and automatically applied during deployments. This removes the risk of manual drift that comes from editing configs through the Felix Web Console.
Directory Structure and Run Modes
All OSGi configuration files live under the ui.config module. The standard path is:
ui.config/
└── src/
└── main/
└── content/
└── jcr_root/
└── apps/
└── <app>/
└── osgiconfig/
├── config/ # All environments
├── config.author/ # Author only
├── config.publish/ # Publish only
├── config.author.dev/ # Author + dev
├── config.author.stage/ # Author + stage
├── config.author.prod/ # Author + prod
├── config.publish.dev/ # Publish + dev
├── config.publish.stage/ # Publish + stage
└── config.publish.prod/ # Publish + prod
Run Mode Resolution
AEM evaluates run mode folders from most specific to least specific. When the same PID appears in multiple folders, the most specific match wins.
| Folder | Active on | Typical use |
|---|---|---|
config/ | All instances | Shared defaults |
config.author/ | All Author instances | Author-only services |
config.publish/ | All Publish instances | Dispatcher/CDN-related settings |
config.author.dev/ | Author in dev | Verbose logging, relaxed security |
config.publish.prod/ | Publish in prod | Production endpoints, strict filters |
config.author.stage/ | Author in stage | Stage-specific integrations |
config.publish.stage/ | Publish in stage | Stage-specific integrations |
Example: If you place a Sling Logging config in both config/ and config.author.dev/, an Author
instance running in the dev environment will use the config.author.dev/ version because it is more
specific.
config.Author/ will not be recognized. Always use lowercase: config.author/.
File Naming Conventions
OSGi configuration files use the .cfg.json extension and follow strict naming rules.
Single (Singleton) Configuration
For a service that has exactly one configuration instance, the filename is the fully qualified PID:
com.example.core.services.impl.MyServiceImpl.cfg.json
Factory Configuration
When a service supports multiple configuration instances (a factory), append a tilde ~ followed by a
unique identifier:
com.example.core.services.impl.MyServiceImpl~site-a.cfg.json
com.example.core.services.impl.MyServiceImpl~site-b.cfg.json
The text after ~ is an arbitrary label — it does not affect behavior but should be descriptive.
A single typo in the PID means the configuration will be silently ignored. Copy-paste the fully qualified class name from the Java source to avoid mistakes.
Configuration Format
The modern standard is .cfg.json — a plain JSON file. Older formats (.config, .cfg) are still
supported but should be avoided in new projects.
Property Types
| JSON type | OSGi type | Example |
|---|---|---|
"string" | String | "https://api.example.com" |
true / false | Boolean | true |
42 | Integer / Long | 100 |
3.14 | Float / Double | 3.14 |
["a","b"] | String[] | ["jpg","png","gif"] |
Full Example
{
"enabled": true,
"syncIntervalMinutes": 30,
"endpoint": "https://dam-api.example.com/v2",
"allowedMimeTypes": [
"image/jpeg",
"image/png",
"application/pdf"
],
"maxRetries": 3,
"connectionTimeoutMs": 5000,
"verboseLogging": false
}
Typed Configuration with Annotations
The recommended approach is to define an annotation interface that describes the configuration schema
and bind it to the component via @Designate.
Step 1 — Define the Configuration Interface
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@ObjectClassDefinition(
name = "Asset Sync Service Configuration",
description = "Controls the remote DAM synchronisation service."
)
public @interface AssetSyncConfig {
@AttributeDefinition(
name = "Enabled",
description = "Enable or disable the sync service."
)
boolean enabled() default false;
@AttributeDefinition(
name = "Sync Interval (minutes)",
description = "How often the sync job runs."
)
int syncIntervalMinutes() default 30;
@AttributeDefinition(
name = "Remote Endpoint",
description = "Base URL of the remote DAM API."
)
String endpoint() default "https://dam-api.example.com/v2";
@AttributeDefinition(
name = "Allowed MIME Types",
description = "List of MIME types that will be synchronised."
)
String[] allowedMimeTypes() default {"image/jpeg", "image/png"};
@AttributeDefinition(
name = "Max Retries",
description = "Number of retry attempts on failure."
)
int maxRetries() default 3;
}
Step 2 — Bind the Configuration to the Service
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(service = AssetSyncService.class, immediate = true)
@Designate(ocd = AssetSyncConfig.class)
@ServiceDescription("Synchronises assets from a remote DAM")
public class AssetSyncServiceImpl implements AssetSyncService {
private static final Logger LOG = LoggerFactory.getLogger(AssetSyncServiceImpl.class);
private boolean enabled;
private int syncInterval;
private String endpoint;
private String[] allowedMimeTypes;
private int maxRetries;
@Activate
@Modified
protected void activate(AssetSyncConfig config) {
this.enabled = config.enabled();
this.syncInterval = config.syncIntervalMinutes();
this.endpoint = config.endpoint();
this.allowedMimeTypes = config.allowedMimeTypes();
this.maxRetries = config.maxRetries();
LOG.info("AssetSyncService activated — enabled={}, interval={}m, endpoint={}",
enabled, syncInterval, endpoint);
}
@Override
public void sync() {
if (!enabled) {
LOG.debug("Sync is disabled, skipping.");
return;
}
// synchronisation logic ...
}
}
Adding @Modified alongside @Activate lets AEM push configuration changes into a running service
without restarting the bundle. Always use both unless the service requires a full restart on config change.
Secret Configuration Values
Sensitive data such as API keys, passwords, and tokens must never be stored in plain text in the repository.
AEM as a Cloud Service
Use Cloud Manager environment variables and reference them with the $[env:...] syntax:
{
"apiKey": "$[env:API_KEY]",
"apiSecret": "$[env:API_SECRET;default=]"
}
Set the actual values in Cloud Manager > Environments > Environment Variables. AEM resolves the placeholders at runtime.
Even if the repository is private, secrets in Git are a liability. Use environment variables or a secrets manager without exception.
AEM 6.5 (on-premise)
On AEM 6.5 you can use the Adobe Granite Crypto Support to encrypt values:
- Navigate to
/system/console/cryptoon the target instance. - Encrypt the secret — the console returns a string starting with
{enc:...}. - Place the encrypted string in the
.cfg.jsonfile.
This approach keeps the plain-text secret out of the repository, but the encryption key is instance-specific — encrypted values cannot be moved between instances without re-encryption.
Felix Web Console
The OSGi Web Console is available on local and non-production AEM instances at:
| URL | Purpose |
|---|---|
/system/console/configMgr | View and edit OSGi configurations |
/system/console/bundles | Inspect bundle states (Active, Resolved, Installed) |
/system/console/components | List Declarative Services components and their status |
/system/console/services | View registered OSGi services |
Any configuration change made through the Felix Console is stored in the JCR at
/apps/system/config and will be overwritten by the next code deployment. Use the console
for debugging only — never as a substitute for repository-deployed configs.
Common AEM Configurations
The table below lists frequently configured services and their PIDs.
| Service | PID | Typical settings |
|---|---|---|
| Sling Referrer Filter | org.apache.sling.security.impl.ReferrerFilter | Allowed hosts, allow empty |
| Link Externalizer | com.day.cq.commons.impl.ExternalizerImpl | Domain mappings per run mode |
| Resource Resolver Mapping | org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl | URL mappings, vanity paths |
| Sling POST Servlet | org.apache.sling.servlets.post.impl.SlingPostServlet | Node name generator, allowed operations |
| Sling Logging | org.apache.sling.commons.log.LogManager | Log level, log file, pattern |
| Sling Logger (factory) | org.apache.sling.commons.log.LogManager.factory.config | Per-package log levels |
| CSRF Filter | com.adobe.granite.csrf.impl.CSRFFilter | Excluded paths |
| CORS Policy | com.adobe.granite.cors.impl.CORSPolicyImpl | Allowed origins, methods, headers |
Common Pitfalls
-
Wrong run mode folder — placing a publish config in
config.author/means it is never active on publish instances. Double-check the folder name. -
Missing tilde in factory configs — without the
~identifiersuffix, AEM treats the file as a singleton and only one instance will ever be created. -
PID typo — the filename PID must exactly match the fully qualified class name annotated with
@Component. A silent mismatch means the config is never applied. -
Editing configs via Felix Console in production — changes are overwritten on the next deployment and create configuration drift. Always deploy configs through the repository.
-
Config not picked up after deployment — verify the content package filter in
ui.config/src/main/content/META-INF/vault/filter.xmlincludes theosgiconfigpath. -
Forgetting the
.cfg.jsonextension — files without the correct extension are ignored by the OSGi installer. -
Using legacy formats in new projects —
.configand.cfgfiles still work but lack JSON tooling support and are harder to read. Prefer.cfg.jsonfor all new configs.
Add a build step that checks OSGi config filenames against the codebase's Java PIDs. A simple script
that compares filenames to @Component annotations catches typos before they reach an environment.