Skip to content

Per-actor policies

The object-storage backend can have store-level defaults for compression + encryption. But sometimes specific actors need different settings:

  • A sensitive actor needs encryption (others don’t).
  • A large-state actor benefits from brotli (others use gzip).
  • A small-state actor opts out of compression.

Override per-actor by implementing the compression() / encryption() methods:

class SensitiveActor extends DurableStateActor<Cmd, State> {
protected encryption() {
return { keyRing: sensitiveKeyRing };
}
protected compression() {
return { algorithm: 'gzip' as const, level: 9 };
}
}

The framework calls these on each persist/load and applies the returned config.

// Store-level config — applies to every actor unless overridden:
const store = new ObjectStorageDurableStateStore({
backend,
compression: { algorithm: 'gzip', level: 6 },
encryption: { keyRing: defaultKeyRing },
});
// Per-actor: override
class MyActor extends DurableStateActor<...> {
protected compression() { return { algorithm: 'none' as const }; }
// encryption() not overridden → uses defaultKeyRing
}

Each method is independently overridable:

  • Return your own config → that config applies.
  • Don’t override → store’s default applies.
  • Return undefined from the method → also store’s default.
class CreditCardActor extends DurableStateActor<...> {
protected encryption() {
return { keyRing: pciKeyRing }; // separate key ring for PCI scope
}
}
class GenericActor extends DurableStateActor<...> {
// No override → uses store default (no encryption, or generic keyRing)
}

PCI / HIPAA / GDPR scope: data classified as sensitive uses a separate master key from non-sensitive. Compromise of one keyRing doesn’t expose the other.

class ChatTranscriptActor extends DurableStateActor<...> {
// Large text state — brotli wins meaningfully
protected compression() {
return { algorithm: 'brotli' as const, level: 8 };
}
}
class CounterActor extends DurableStateActor<...> {
// Tiny state — compression overhead exceeds savings
protected compression() {
return { algorithm: 'none' as const };
}
}

Match the compression to the data shape. Default gzip is a safe middle ground; per-actor tuning extracts more.

DurableStateActor.persist(state)
serializer.toBinary(state) → JSON or CBOR bytes
this.compression() → applies if not 'none'
this.encryption() → applies if set
backend.put(key, ciphertext, opts)

The framework looks up the actor’s overrides at each persist — so even if you change the override values dynamically (rare), new writes use new settings.

DurableStateActor.preStart
backend.get(key) → bytes + metadata
detect encryption from metadata (key-id) → decrypt
detect compression from metadata (Content-Encoding) → decompress
serializer.fromBinary → state

Reads use the metadata stored with the object, not the actor’s current overrides. An object encrypted under pciKeyRing is decrypted via pciKeyRing whether or not the actor’s current encryption() says so.

This means: rotating an actor’s policy doesn’t break old data as long as the necessary keys / algorithms are available.

// Old: no encryption.
// New: encryption under pciKeyRing.
class CreditCardActor extends DurableStateActor<...> {
protected encryption() {
return { keyRing: pciKeyRing };
}
}

Old (unencrypted) reads work — no metadata → plaintext path. New writes encrypt. Eventually:

  • A re-encryption sweep filtered to this actor’s persistenceIds migrates the rest.

See key rotation.

class Account extends PersistentActor<Cmd, Event, State> {
// Per-actor compression for snapshots:
protected compression() {
return { algorithm: 'brotli' as const };
}
// Per-actor encryption for snapshots:
protected encryption() {
return { keyRing: accountKeyRing };
}
}

Same methods apply when the snapshot store is backed by object-storage. Persistent actors and durable-state actors both honor them.