SOASAP Angular SDK

Local-first feature flag evaluation for Angular 17+ — reactive Signals, browser cache, and real-time SSE updates.

Overview

The SOASAP Angular SDK synchronizes feature flag configuration from SOASAP Cloud in the background and evaluates flags from an in-memory snapshot. Flag reads are synchronous, O(1), and never perform network or storage I/O on the hot path. Angular Signals update when the snapshot changes.

The SDK provides:

  • Local evaluation — dictionary lookups against an immutable snapshot
  • Reactive SignalsSoasapService.bool, number, string, and json return computed signals
  • Real-time synchronization — Server-Sent Events (SSE) delta updates
  • Persistent cachelocalStorage-backed snapshot for return visits
  • Offline operation — last known snapshot when the network or control plane is unavailable
  • Explicit defaults — every signal and getter accepts a code-defined fallback
  • Non-blocking startup — optional preload: true starts sync without blocking bootstrap

Runtime characteristics

  • O(1) local evaluation — reads from an in-memory snapshot; no network requests during evaluation
  • Reactive updates — snapshot swaps notify subscribers; computed signals re-evaluate when flag values change
  • Real-time SSE synchronization — background worker applies validated payloads with atomic configuration updates
  • Persistent browser cachelocalStorage snapshot for fast cold starts and return visits
  • Offline operation — last known snapshot when the network or control plane is unavailable
  • Explicit defaults — missing, mistyped, or unsynced keys return your code-defined fallback
  • Non-blocking startuppreload: true starts synchronization without blocking Angular bootstrap
Production behavior. Getters and signals never throw on sync or evaluation failures. Injecting SoasapService without provideSoasap fails at runtime with a missing provider error. Zero runtime dependencies — @angular/core is a peer dependency.

Requirements

Supported runtimes:

  • Angular 17, 18, 19+ (@angular/core >= 17 peer dependency)
  • Modern browsers with fetch, ReadableStream, and localStorage

Supported application types:

  • Standalone components with bootstrapApplication
  • app.config.ts provider registration
  • Client-rendered SPAs built with the Angular CLI
  • SSR and hybrid apps (browser-only sync; server platform disables storage automatically)

Requires outbound HTTPS to api.soasap.com for SSE synchronization from the browser.

Installation

npm:

npm install @soasap-com/angular-sdk

pnpm:

pnpm add @soasap-com/angular-sdk

yarn:

yarn add @soasap-com/angular-sdk

package.json dependency:

{
  "dependencies": {
    "@soasap-com/angular-sdk": "1.0.3",
    "@angular/core": "^17.0.0"
  }
}

Verify installation

npm list @soasap-com/angular-sdk

Confirm @soasap-com/angular-sdk appears in the project dependency tree before continuing.

Before you begin

You will need:

  • A SOASAP account
  • A project
  • An environment
  • An environment API key

If you have not completed these steps, start with Quick Start.

Client-side API keys. Environment API keys are embedded in browser bundles and visible to end users. Use separate keys per environment and treat them as client-scoped credentials, not server secrets.

Installation flow

After your project and environment exist in the dashboard, install the npm package and register provideSoasap at application bootstrap. The API key binds the browser session to a single environment. Synchronization runs in the background; components evaluate flags locally via Signals or the imperative client.

Application registration

Register the SDK once at bootstrap and enable preload for production SPAs:

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideSoasap } from '@soasap-com/angular-sdk';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';

if (!environment.soasapApiKey) {
  throw new Error('soasapApiKey is required in environment configuration.');
}

bootstrapApplication(AppComponent, {
  providers: [
    provideSoasap({
      apiKey: environment.soasapApiKey,
      preload: true,
    }),
  ],
});

Or in app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideSoasap } from '@soasap-com/angular-sdk';
import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    provideSoasap({
      apiKey: environment.soasapApiKey,
      preload: true,
    }),
  ],
};
  • API key — environment-scoped credential sent as X-API-Key on SSE requests; identifies which configuration to synchronize
  • One client per tab — single client instance per browser tab or app shell
  • Preload — with preload: true, the SSE worker starts during client construction without blocking bootstrap
  • Auto cleanup — when passing options, provideSoasap calls close() when the injector is destroyed

Pass either SoasapClientOptions (SDK creates the client) or a pre-built client from createSoasapClient.

Configuration

Store the API key in Angular environment files or build-time replacements. The SDK does not read .env files directly — configure through your build pipeline.

environment.ts:

export const environment = {
  production: false,
  soasapApiKey: 'soasap...',
};
provideSoasap({
  apiKey: environment.soasapApiKey,
  preload: true,
})

Shared client instance (recommended for testability):

import { createSoasapClient, provideSoasap } from '@soasap-com/angular-sdk';

const flags = createSoasapClient({
  apiKey: environment.soasapApiKey,
  preload: true,
});

export const appConfig: ApplicationConfig = {
  providers: [provideSoasap(flags)],
};

Optional configuration:

import { SoasapErrorSource, provideSoasap } from '@soasap-com/angular-sdk';

provideSoasap({
  apiKey: environment.soasapApiKey,
  baseUrl: 'https://api.soasap.com',
  cacheKeyPrefix: 'myapp:flags',
  preload: true,
  onError: (ctx) => {
    console.warn(
      `[SOASAP ${ctx.source}] transient=${ctx.isTransient}`,
      ctx.exception.message,
    );
  },
});

Custom storage (for tests or sessionStorage): pass a storage object implementing getItem, setItem, and removeItem.

Evaluating flags

Inject SoasapService in components for reactive Signals. Use the imperative client for guards, interceptors, and non-UI code paths.

Signals (reactive)

Flag type Method Default
Booleanboolfalse
Stringstring''
Numbernumber0
JSONjson<T>undefined
import { Component, inject, type Signal } from '@angular/core';
import { SoasapService } from '@soasap-com/angular-sdk';

@Component({
  selector: 'app-checkout',
  template: `
    @if (newCheckout()) {
      <app-new-checkout />
    } @else {
      <app-legacy-checkout />
    }
  `,
})
export class CheckoutComponent {
  private readonly soasap = inject(SoasapService);

  readonly newCheckout: Signal<boolean> = this.soasap.bool('new-checkout', false);
  readonly maxItems: Signal<number> = this.soasap.number('max-items', 10);
  readonly theme: Signal<string> = this.soasap.string('checkout-theme', 'default');
}

Signal methods return computed signals — read values in templates with () (for example, newCheckout()). flag is an alias for bool.

Imperative reads

SoasapService also exposes sync getters for use in methods, effects, and guards:

Flag type SoasapService SoasapClient
BooleangetBoolgetBool
StringgetStringgetString
NumbergetNumbergetNumber
JSONgetJson<T>getJson<T>
import { inject } from '@angular/core';
import { injectSoasapClient, SoasapService } from '@soasap-com/angular-sdk';

// Via service
const soasap = inject(SoasapService);
if (soasap.getBool('beta-feature', false)) { /* ... */ }

// Direct client access
const client = injectSoasapClient();
if (client.getBool('beta-feature', false)) { /* ... */ }

getFlag is an alias for getBool. getClient() on SoasapService returns the underlying SoasapClient.

JSON

Best practice. Use TypeScript interfaces or typed object literals whenever possible instead of untyped mappings. Strong typing improves discoverability, validation, refactoring safety, and maintainability.
interface CheckoutConfig {
  enableUpsells: boolean;
  maxItems: number;
}
readonly config: Signal<CheckoutConfig | undefined> =
  this.soasap.json<CheckoutConfig>('checkout-config', {
    enableUpsells: false,
    maxItems: 10,
  });

JSON keys from the dashboard are returned as stored (typically camelCase). getJson returns a deep copy; non-object values or copy failures return the default.

Explicit defaults

Always pass a default on every signal and getter read:

this.soasap.bool('new-checkout', false)

The default is returned when:

  • The flag key does not exist in the synchronized snapshot
  • The flag was deleted or disabled in the dashboard for this environment
  • The page loads before the first SSE payload arrives and no browser cache exists
  • A restored localStorage cache is empty or invalid
  • The flag value type does not match the getter (for example, a string flag read with bool)
  • Synchronization is temporarily delayed after deploy or reconnect
  • During SSR — the server platform disables storage; signals return explicit defaults until client hydration

Explicit defaults are your production safety net. They define safe behavior when configuration is missing, stale, or not yet synchronized — without try/catch on the hot path.

Rule of thumb. If a flag controls production behavior, the default should be the safest possible outcome — usually false for feature gates and conservative values for limits.

Runtime architecture

  • SSE delivers full or delta flag payloads to a background worker in the browser
  • Valid payloads replace the in-memory snapshot atomically — readers never observe partial updates
  • Snapshot swaps call subscribe listeners; SoasapService bumps an internal version signal that invalidates computed flag signals
  • Evaluation reads a local reference to an immutable snapshot with no locks on the render path
  • No HTTP requests and no blocking storage I/O during signal evaluation or getter calls

Implementation details on the hot path:

  • Snapshot replacement swaps an object reference — concurrent readers keep the previous snapshot until the swap completes
  • Each getter copies the current snapshot reference locally before lookup
  • SSE parser enforces a 5 MB payload cap; invalid root payloads are ignored

Persistent cache

On construction, the client synchronously loads the last persisted JSON snapshot from browser storage before any network activity. SSE updates enqueue debounced storage writes (coalesced every ~2.5 seconds).

Default storage key format:

soasap:cache:<api-key-prefix>

The prefix is derived from the first 10 characters of the API key (after stripping a leading soasap literal). Override the prefix:

createSoasapClient({
  apiKey: '...',
  cacheKeyPrefix: 'myapp:flags',
});

On the server platform (provideSoasap with options), storage is automatically disabled — the client runs in memory-only mode during SSR. Browser hydration loads localStorage on the client.

Return visits and reloads — the UI hydrates from localStorage immediately; SSE reconnects in the background. First visit without cache returns defaults until the first successful sync.

Offline operation

Reads are decoupled from sync. Flag evaluation continues normally during outages because reads never depend on network availability. Only synchronization is affected during disconnects.

The SDK continues evaluating flags from the last known snapshot when:

  • Internet connectivity is unavailable
  • SOASAP Cloud returns errors or is unreachable
  • The SSE connection drops (proxy timeout, firewall, transient network failure)

The background worker reconnects with exponential backoff. Reads are unaffected during disconnect — only future updates are delayed.

Stale configuration is preferable to unavailable configuration: the application keeps running with the last known values and your code defaults.

ScenarioBehavior
API unavailableServes stale cached snapshot
SSE disconnectedKeeps last known snapshot; reconnects with backoff
First visit, no cacheReturns explicit defaults until first sync
Invalid SSE payloadPayload ignored; snapshot unchanged
Storage quota / failureIn-memory mode continues; error reported via onError

Preload and startup behavior

preload: true is recommended for production SPAs.

With preload:

  1. Client construction loads the browser cache synchronously (on the browser platform)
  2. The SSE worker starts immediately — no blocking during bootstrapApplication
  3. The app shell renders while synchronization completes in the background
  4. First paint may use cached flags or explicit defaults until the first SSE payload arrives

Without preload (lazy mode):

provideSoasap({ apiKey: environment.soasapApiKey })

The SSE worker starts on the first flag read (signal evaluation or getter call). Until then, evaluation uses the browser cache (if present) or your explicit defaults. Prefer preload for predictable synchronization timing after deploy.

SSR and hydration

When registered with options, provideSoasap disables browser storage on the server platform automatically. During SSR, signals and getters return your explicit defaults — not live flag values from the network.

After client hydration, the browser client loads localStorage, connects SSE, and signals update when the snapshot changes. Design defaults to match your loading state, or defer flag-dependent UI until after hydration.

Graceful shutdown

Call close() to cancel SSE, flush storage (up to 5 seconds), and release resources:

await flags.close();

provideSoasap({ apiKey, ... }) registers a destroy hook that calls close() automatically. When passing a pre-built client via provideSoasap(client), you are responsible for calling close() when done.

Best practices

  • Register once at bootstrap — add provideSoasap to root providers in main.ts or app.config.ts.
  • One client per tab — single client instance per browser tab; do not register per route or lazy-loaded module.
  • Enable preloadpreload: true for non-blocking background sync at startup.
  • Always pass explicit defaults — safe behavior when keys are missing or not yet synchronized.
  • Use Signals in templates — assign signal properties on the component class; call signal() in templates.
  • Use imperative getters in guards — inject SoasapService or injectSoasapClient() in route guards and resolvers. See Route Guards.
  • Separate keys per environment — Development, Staging, and Production hold independent configuration.
  • Do not register multiple providers — avoids duplicate SSE connections unless intentionally isolating injectors.

Common issues

NullInjectorError for SoasapService

  • Ensure provideSoasap is registered in the application root providers
  • Confirm the component is rendered within the bootstrapped application injector tree

Flag always returns the default

  • Verify the API key matches the intended environment
  • Confirm the flag key exists and is enabled in that environment
  • Check the flag type matches the method (bool for boolean flags)

Dashboard changes do not appear

  • Verify outbound HTTPS to api.soasap.com and SSE is not blocked by a proxy, firewall, or browser extension
  • See SSE Disconnected

Wrong value returned

  • Verify the flag value in the selected environment — Development and Production are independent
  • Confirm the deployed build uses the API key for that environment (check environment file replacement)

Multiple SDK instances

Symptoms

  • Multiple SSE connections from the same browser tab
  • Increased memory usage
  • Duplicate synchronization traffic

Resolution — register provideSoasap once at bootstrap and reuse the same client. Do not call createSoasapClient inside individual components or feature modules.

Startup synchronization delays

Expected with preload: the app renders immediately; the first SSE payload may arrive seconds after load. Until then, evaluation uses the browser cache or explicit defaults. This is by design — startup is never blocked on sync.

Complete example

The following example demonstrates the recommended production setup for Angular standalone applications, including bootstrap registration, preload, reactive Signals, and template evaluation.

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideSoasap } from '@soasap-com/angular-sdk';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { environment } from './environments/environment';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideSoasap({
      apiKey: environment.soasapApiKey,
      preload: true,
    }),
  ],
});
// checkout.component.ts
import { Component, inject, type Signal } from '@angular/core';
import { SoasapService } from '@soasap-com/angular-sdk';

@Component({
  selector: 'app-checkout',
  standalone: true,
  template: `
    @if (newCheckout()) {
      <section>
        <h1>New checkout</h1>
        <p>Max items: {{ maxItems() }}</p>
        <p>Theme: {{ theme() }}</p>
      </section>
    } @else {
      <section>
        <h1>Legacy checkout</h1>
      </section>
    }
  `,
})
export class CheckoutComponent {
  private readonly soasap = inject(SoasapService);

  readonly newCheckout: Signal<boolean> = this.soasap.bool('new-checkout', false);
  readonly maxItems: Signal<number> = this.soasap.number('max-items', 10);
  readonly theme: Signal<string> = this.soasap.string('checkout-theme', 'default');
}

Production recommendations

  • Inject API keys at build time via environment file replacement — never hardcode Production keys in source
  • Use separate API keys per environment — never share Production keys with local Development builds
  • Treat client-side API keys as public credentials scoped to a single environment
  • Keep explicit defaults in code for every flag read — defaults define behavior during outages and SSR
  • Register onError in Production to log background sync, storage, and parser failures without affecting reads
  • Register provideSoasap once at bootstrap; avoid duplicate clients per tab
  • Monitor browser connectivity and SSE health in Production user sessions where possible

Next steps

  • Feature Flags — flag types, defaults, and naming
  • Architecture — local evaluation, SSE, cache, and offline operation
  • API Keys — issuing, rotating, and securing environment keys
  • Other SDKs — .NET, Node.js, React, and more

Deep dives: provideSoasap, Signals, Offline Behavior.