Skip to main content

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.

Why code-deployed configs matter

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.

FolderActive onTypical use
config/All instancesShared defaults
config.author/All Author instancesAuthor-only services
config.publish/All Publish instancesDispatcher/CDN-related settings
config.author.dev/Author in devVerbose logging, relaxed security
config.publish.prod/Publish in prodProduction endpoints, strict filters
config.author.stage/Author in stageStage-specific integrations
config.publish.stage/Publish in stageStage-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.

Run mode folder names are case-sensitive

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.

PID must match the class name exactly

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 typeOSGi typeExample
"string"String"https://api.example.com"
true / falseBooleantrue
42Integer / Long100
3.14Float / Double3.14
["a","b"]String[]["jpg","png","gif"]

Full Example

com.example.core.services.impl.AssetSyncServiceImpl.cfg.json
{
"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

AssetSyncConfig.java
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

AssetSyncServiceImpl.java
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 ...
}
}
@Modified for live updates

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:

com.example.core.services.impl.ApiClientImpl.cfg.json
{
"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.

Never commit real secrets

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:

  1. Navigate to /system/console/crypto on the target instance.
  2. Encrypt the secret — the console returns a string starting with {enc:...}.
  3. Place the encrypted string in the .cfg.json file.

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:

URLPurpose
/system/console/configMgrView and edit OSGi configurations
/system/console/bundlesInspect bundle states (Active, Resolved, Installed)
/system/console/componentsList Declarative Services components and their status
/system/console/servicesView registered OSGi services
Console changes are lost on deployment

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.

ServicePIDTypical settings
Sling Referrer Filterorg.apache.sling.security.impl.ReferrerFilterAllowed hosts, allow empty
Link Externalizercom.day.cq.commons.impl.ExternalizerImplDomain mappings per run mode
Resource Resolver Mappingorg.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImplURL mappings, vanity paths
Sling POST Servletorg.apache.sling.servlets.post.impl.SlingPostServletNode name generator, allowed operations
Sling Loggingorg.apache.sling.commons.log.LogManagerLog level, log file, pattern
Sling Logger (factory)org.apache.sling.commons.log.LogManager.factory.configPer-package log levels
CSRF Filtercom.adobe.granite.csrf.impl.CSRFFilterExcluded paths
CORS Policycom.adobe.granite.cors.impl.CORSPolicyImplAllowed origins, methods, headers

Common Pitfalls

  1. Wrong run mode folder — placing a publish config in config.author/ means it is never active on publish instances. Double-check the folder name.

  2. Missing tilde in factory configs — without the ~identifier suffix, AEM treats the file as a singleton and only one instance will ever be created.

  3. 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.

  4. Editing configs via Felix Console in production — changes are overwritten on the next deployment and create configuration drift. Always deploy configs through the repository.

  5. Config not picked up after deployment — verify the content package filter in ui.config/src/main/content/META-INF/vault/filter.xml includes the osgiconfig path.

  6. Forgetting the .cfg.json extension — files without the correct extension are ignored by the OSGi installer.

  7. Using legacy formats in new projects.config and .cfg files still work but lack JSON tooling support and are harder to read. Prefer .cfg.json for all new configs.

Validate configs in CI

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.


See also