npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

featurefly

v0.3.2

Published

Lightweight, framework-agnostic Feature Flags SDK with built-in caching, retry, circuit breaker, targeting, A/B testing, and real-time streaming

Readme

FeatureFly 🚀

Lightweight, universal Feature Flags SDK for Node.js and the browser. One package. Backend and frontend. Zero config to start.

CI npm version TypeScript License: MIT Bundle Size Bundle Size


📑 Table of Contents


📦 Installation

npm install featurefly

Zero runtime dependencies! FeatureFly has no runtime dependencies. Only install your framework if you want to use the framework-specific hooks:

# React projects (optional - only if you want hooks)
npm install featurefly react

# Vue 3 projects
npm install featurefly vue

Tree-Shakeable Entry Points

FeatureFly supports granular imports for optimal bundle size. All sizes are gzipped.

| Entry Point | Gzipped Size | Lazy Loaded | Use Case | |-------------|--------------|-------------|----------| | featurefly | ~11KB core | ~13KB on-demand | Complete SDK, backwards compatible | | featurefly/core | ~11KB | - | Vanilla JS, Node.js, Serverless | | featurefly/react | ~11KB + ~2KB | - | React 18+ applications | | featurefly/vue | ~11KB + ~2KB | - | Vue 3 applications | | featurefly/advanced | - | ~13KB | EdgeEvaluator, Streaming, Metrics, targeting |

Note: Core bundle (~11KB gzipped) includes Client + Cache + Retry + Circuit Breaker + Event Emitter + Logger. Advanced features (EdgeEvaluator, targeting, streaming, metrics, experiments) are lazy loaded on-demand when first used.

// Minimal - tree-shakeable core (~11KB gzipped)
import { FeatureFlagsClient } from 'featurefly/core';

// React - hooks included (~13KB gzipped total)
import { FeatureFlyProvider, useFeatureFlag } from 'featurefly/react';

// Advanced features loaded on-demand (~13KB additional, loaded once)
import { EdgeEvaluator, FlagStreamClient } from 'featurefly/advanced';

🚀 Quick Start

import { FeatureFlagsClient } from "featurefly";

const client = new FeatureFlagsClient({
  baseUrl: "https://your-api.com",
  apiKey: "your-api-key",
});

const isEnabled = await client.evaluateFlag("new-checkout-flow");

if (isEnabled) {
  // New checkout
} else {
  // Legacy checkout
}

// Always dispose when done (servers: on shutdown, SPAs: on unmount)
client.dispose();

That's it. The same code works in a NestJS service, an Express middleware, a Vite frontend, or a Next.js API route.


🌍 Usage by Environment

FeatureFly is universal — the same npm package runs on the server and in the browser. The only difference is how you integrate it.

🖥️ Backend (Node.js / NestJS / Express)

Use the client directly to evaluate flags on the server side, typically in middleware, guards, or services.

// services/feature-flags.service.ts
import { FeatureFlagsClient } from "featurefly";

const client = new FeatureFlagsClient({
  baseUrl: process.env.FEATURE_FLAGS_API_URL,
  apiKey: process.env.FEATURE_FLAGS_API_KEY,
  cacheTtlMs: 30_000, // Cache flags for 30s on the server
});

export async function isFeatureEnabled(
  slug: string,
  userId?: string,
  workspaceId?: string,
): Promise<boolean> {
  return client.evaluateFlag(slug, { userId, workspaceId });
}
// Example: Express middleware
app.get("/dashboard", async (req, res) => {
  const showNewDashboard = await isFeatureEnabled(
    "new-dashboard",
    req.user.id,
    req.user.workspaceId,
  );

  if (showNewDashboard) {
    return res.render("dashboard-v2");
  }
  return res.render("dashboard");
});
// Example: NestJS guard
@Injectable()
export class FeatureFlagGuard implements CanActivate {
  constructor(private readonly flags: FeatureFlagsClient) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    return this.flags.evaluateFlag("beta-api", {
      userId: request.user.id,
    });
  }
}

💡 Tip: On the server, create a single instance of FeatureFlagsClient and reuse it across requests. Call client.dispose() during graceful shutdown.


🌐 Frontend (Vanilla JS / Any bundler)

Works with any bundler (Vite, Webpack, esbuild, Rollup) or even a plain <script> tag.

import { FeatureFlagsClient } from "featurefly";

const client = new FeatureFlagsClient({
  baseUrl: "https://your-api.com",
  apiKey: "pk_live_xxx", // Use a public/client key
  streaming: true, // Auto-receive flag updates via SSE
});

// Evaluate and render
const showBanner = await client.evaluateFlag("promo-banner", {
  userId: currentUser.id,
  attributes: { plan: currentUser.plan, country: "AR" },
});

if (showBanner) {
  document.getElementById("promo")!.style.display = "block";
}

// React to live flag changes
client.on("flagsUpdated", async () => {
  const updated = await client.evaluateFlag("promo-banner");
  document.getElementById("promo")!.style.display = updated ? "block" : "none";
});

⚛️ React

Note: Requires React 16.8+ (hooks support).

Import from featurefly/react. Hooks auto-re-evaluate when flags change via streaming.

import { FeatureFlagsClient } from "featurefly";
import {
  FeatureFlyProvider,
  useFeatureFlag,
  useAllFlags,
} from "featurefly/react";

// Create your client (once, outside the component tree)
const client = new FeatureFlagsClient({
  baseUrl: "https://your-api.com",
  apiKey: "pk_live_xxx",
  streaming: true,
});

// 1. Wrap your app
function App() {
  return (
    <FeatureFlyProvider client={client}>
      <MyComponent />
    </FeatureFlyProvider>
  );
}

// 2. Use hooks
function MyComponent() {
  const { value: darkMode, loading } = useFeatureFlag("dark-mode", false);
  const { flags } = useAllFlags({ workspaceId: "ws-123" });

  if (loading) return <Spinner />;

  return (
    <div className={darkMode ? "dark" : "light"}>
      {flags["new-feature"] && <NewFeature />}
    </div>
  );
}

| Hook | Returns | Description | | ---------------------------------------------- | -------------------- | ----------------------------------------------- | | useFeatureFlag(slug, defaultValue, context?) | { value, loading } | Evaluates a single flag. Re-renders on changes. | | useAllFlags(context?) | { flags, loading } | Returns all flags as a key-value object. |


💚 Vue 3

Note: Requires Vue 3.0+ (Composition API support).

Import from featurefly/vue. Composables return reactive Ref values that update automatically.

// main.ts
import { createApp } from "vue";
import { FeatureFlagsClient } from "featurefly";
import { FeatureFlyPlugin } from "featurefly/vue";

const client = new FeatureFlagsClient({
  baseUrl: "https://your-api.com",
  apiKey: "pk_live_xxx",
  streaming: true,
});

const app = createApp(App);
app.use(FeatureFlyPlugin, { client });
app.mount("#app");
<!-- MyComponent.vue -->
<script setup>
import { useFeatureFlag, useAllFlags } from "featurefly/vue";

const darkMode = useFeatureFlag("dark-mode", false);
const flags = useAllFlags({ workspaceId: "ws-123" });
</script>

<template>
  <div :class="{ dark: darkMode }">
    <NewFeature v-if="flags['new-feature']" />
  </div>
</template>

| Composable | Returns | Description | | ---------------------------------------------- | -------------------------------- | ------------------------------------------ | | useFeatureFlag(slug, defaultValue, context?) | Ref<T> | Reactive ref that updates on flag changes. | | useAllFlags(context?) | Ref<Record<string, FlagValue>> | Reactive ref with all flags. |


🔧 Configuration

All options are optional except baseUrl.

const client = new FeatureFlagsClient({
  // Required — your feature flags API endpoint
  baseUrl: "https://your-api.com",

  // Authentication
  apiKey: "your-api-key",

  // HTTP timeout (default: 10000ms)
  timeout: 10_000,

  // Cache (default: enabled, 60s TTL)
  cacheEnabled: true,
  cacheTtlMs: 60_000,

  // Retry (default: 3 attempts, 1s base delay)
  retry: {
    maxAttempts: 3,
    baseDelayMs: 1_000,
    maxDelayMs: 10_000,
  },

  // Circuit Breaker (default: 5 failures, 30s reset)
  circuitBreaker: {
    failureThreshold: 5,
    resetTimeoutMs: 30_000,
  },

  // Logging — 'debug' | 'info' | 'warn' | 'error' | 'silent' (default: 'warn')
  logLevel: "warn",

  // Or provide your own logger (pino, winston, etc.)
  logger: myLogger,

  // Local overrides — flags evaluated instantly without HTTP
  localOverrides: {
    "feature-x": true,
    "variant-test": "blue",
  },

  // Pre-evaluated flags to hydrate the client instantly (e.g. injected by Next.js/SSR)
  bootstrapFlags: {
    "new-checkout": true
  },

  // Fallback defaults when the API is unreachable
  fallbackDefaults: {
    "critical-feature": false,
  },

  // Custom static headers merged into every request
  headers: {
    "x-workspace-id": "ws-123",
    "x-custom-header": "value",
  },

  // Dynamic request interceptor — invoked before every HTTP call
  // Perfect for JWT auth where tokens rotate over time
  requestInterceptor: () => ({
    Authorization: `Bearer ${getAccessToken()}`,
    "x-user-id": getUserId(),
  }),

  // Send cookies with cross-origin requests (default: false)
  withCredentials: true,

  // Real-time updates via SSE (default: false)
  streaming: true, // or { reconnectDelayMs: 2000 }

  // A/B testing callback
  trackingCallback: (assignment) => {
    analytics.track("Experiment Viewed", assignment);
  },

  // Edge evaluation — pass a pre-fetched document for fully offline mode
  // edgeDocument: myPreFetchedDoc,
});

📚 API Reference

Flag Evaluation

// Boolean flag
const isEnabled = await client.evaluateFlag("my-flag");

// With user context (targeting, rollout, experiments)
const isEnabled = await client.evaluateFlag("my-flag", {
  workspaceId: "ws-123",
  userId: "user-456",
  attributes: { plan: "pro", country: "AR" },
});

// Typed flag values
const variant = await client.evaluateFlag<string>("ab-test");
const limit = await client.evaluateFlag<number>("rate-limit");
const config = await client.evaluateFlag<Record<string, unknown>>("ui-config");

// All flags at once (single HTTP request)
const allFlags = await client.evaluateAllFlags({ workspaceId: "ws-123" });

Flag Management (CRUD)

// Create
const flag = await client.createFlag({
  slug: "new-feature",
  name: "New Feature",
  category: "both",
  valueType: "boolean",
  defaultValue: false,
  tags: ["v2", "beta"],
});

// Read
const allFlags = await client.getAllFlags();
const byId = await client.getFlagById("flag-id");
const bySlug = await client.getFlagBySlug("my-flag");

// Update
await client.updateFlag("flag-id", { name: "Updated Name" });

// Delete
await client.deleteFlag("flag-id");

Workspace Flags

// Set a workspace-level override
await client.setWorkspaceFlag("feature-x", "workspace-123", true);

// Get all flags for a workspace
const flags = await client.getWorkspaceFlags("workspace-123");

// Remove an override
await client.removeWorkspaceFlag("feature-x", "workspace-123");

Real-time Streaming (SSE)

// Option A: Auto-start via config
const client = new FeatureFlagsClient({
  baseUrl: "https://your-api.com",
  streaming: true,
});

// Option B: Manual control
client.startStreaming();
client.stopStreaming();

// React to flag updates (cache is auto-invalidated)
client.on("flagsUpdated", () => {
  console.log("Flags refreshed from server!");
});

Edge Evaluation (Offline Mode)

Evaluate flags with 0ms latency by pre-fetching the entire flag document.

// 1. Fetch the document (e.g., at server startup or on app boot)
const doc = await fetch("https://your-api.com/feature-flags/document").then(
  (r) => r.json(),
);

// 2. Create a fully offline client
const edgeClient = new FeatureFlagsClient({
  baseUrl: "https://your-api.com",
  edgeDocument: doc,
});

// 3. Evaluate — zero network calls, zero latency
const value = await edgeClient.evaluateFlag("my-flag", {
  userId: "user-123",
});

SSR & Bootstrapping (Zero-Flicker)

For Server-Side Rendering (Next.js, Nuxt, Remix), you can inject pre-evaluated flags from the server to the client. This hydrates the client cache instantly, ensuring loading is false on the first render and avoiding any UI flickering.

// 1. Server evaluates flags and injects them into the HTML
const serverFlags = await serverClient.evaluateAllFlags({ userId });
window.__FEATURE_FLAGS__ = serverFlags;

// 2. Client initializes with bootstrap flags (no initial HTTP request made)
const client = new FeatureFlagsClient({
  baseUrl: "https://your-api.com",
  apiKey: "pk_live_xxx",
  bootstrapFlags: window.__FEATURE_FLAGS__,
});

Impact Metrics

Client-side telemetry collected passively, no external calls.

// Get metrics snapshot (async - loads ImpactMetrics module on-demand)
const metrics = await client.getImpactMetrics();

if (metrics) {
  console.log(metrics.totalEvaluations); // 1523
  console.log(metrics.cacheHitRate); // 0.87
  console.log(metrics.latency.p50); // 2ms
  console.log(metrics.latency.p95); // 12ms
  console.log(metrics.latency.p99); // 45ms

  // Per-flag detail
  console.log(metrics.flags["my-flag"].evaluations); // 42

  // Experiment exposures
  console.log(metrics.experiments["checkout-exp"].exposures); // 300
}

// Reset all counters (async)
await client.resetMetrics();

Analytics

// Get flag statistics overview
const stats = await client.getFlagStats();
console.log(stats.totalFlags);
console.log(stats.enabledFlags);

// Get flags filtered by category
const frontendFlags = await client.getFlagsByCategory('frontend');
const backendFlags = await client.getFlagsByCategory('backend');
const bothFlags = await client.getFlagsByCategory('both');

// Get flags by target service
const apiFlags = await client.getFlagsByTargetService('api-gateway');

Event System

// Subscribe to events
client.on("flagEvaluated", ({ slug, value, reason, durationMs }) => {
  analytics.track("flag_check", { slug, value, reason, durationMs });
});

// Flag value changed (detect drift)
client.on("flagChanged", ({ slug, previousValue, newValue }) => {
  console.log(`${slug}: ${previousValue} → ${newValue}`);
});

// Circuit breaker
client.on("circuitOpen", ({ state, failures }) => {
  alerting.send(`Circuit opened after ${failures} failures`);
});
client.on("circuitClosed", () => console.log("Circuit closed"));
client.on("circuitHalfOpen", () => console.log("Circuit half-open"));

// Cache
client.on("cacheHit", ({ key }) => monitor.increment("cache.hit"));
client.on("cacheMiss", ({ key }) => monitor.increment("cache.miss"));
client.on("cacheCleared", () => console.log("Cache cleared"));

// Streaming
client.on("streamConnected", () => console.log("SSE connected"));
client.on("streamDisconnected", () => console.log("SSE lost"));
client.on("flagsUpdated", ({ slugs }) => console.log(`Flags updated: ${slugs?.join(', ') || 'all'}`));

// Request failures
client.on("requestFailed", ({ endpoint, error, attempt }) => {
  console.error(`Request to ${endpoint} failed (attempt ${attempt}): ${error}`);
});

// Listener errors
client.on("listenerError", ({ event, error }) => {
  console.error(`Error in ${event} listener:`, error);
});

// Subscribe once (auto-unsubscribes after first event)
client.once("flagEvaluated", ({ slug }) => {
  console.log(`First evaluation for ${slug}`);
});

// Unsubscribe
const unsubscribe = client.on("flagEvaluated", handler);
unsubscribe();

Local Overrides

Useful for development and testing — evaluated instantly, no HTTP.

client.setLocalOverride("experimental-ui", true);
client.setLocalOverride("theme", "dark");

const overrides = client.getLocalOverrides();

client.removeLocalOverride("experimental-ui");
client.clearLocalOverrides();

Cache Access (Synchronous)

// Get a single cached flag value (sync, no HTTP)
// Returns undefined if not in cache
const cachedValue = client.getCachedFlag<boolean>("my-flag", { userId: "123" });
if (cachedValue !== undefined) {
  console.log("From cache:", cachedValue);
}

// Get all cached batch-evaluated flags (sync)
// Returns undefined if no batch evaluation was cached
const allCached = client.getCachedFlags({ workspaceId: "ws-123" });
if (allCached) {
  console.log("All cached flags:", allCached);
}

// Clear cache
client.clearCache();

// Get cache statistics
const stats = client.getCacheStats();
console.log(`Cache size: ${stats.size}, keys: ${stats.keys.length}, enabled: ${stats.enabled}`);

Edge Evaluation (Advanced)

// Load edge document for offline evaluation mode
// After calling this, evaluations happen locally (zero HTTP)
await client.loadEdgeDocument();

// Check if streaming is active
client.startStreaming(); // Start SSE connection
client.stopStreaming(); // Stop SSE connection

Utilities

// Circuit breaker
client.getCircuitBreakerState(); // { state, 'open'|'closed'|'half-open', failures }
client.resetCircuitBreaker(); // Manually reset to closed state

// Lifecycle
client.dispose(); // Release timers, listeners, metrics, SSE connections
client.isDisposed(); // true if disposed

⚠️ Considerations

Universal Package (Backend + Frontend)

FeatureFly ships as a single universal package that works on both the server and the browser:

| Concern | How it's handled | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | HTTP client | Uses native fetch, which auto-detects the environment (Node.js http module vs browser XMLHttpRequest). | | Hashing (MurmurHash3) | Implemented in pure TypeScript — no dependency on Node.js crypto. | | SSE Streaming | Uses the native browser EventSource API. On Node.js < 22, streaming is disabled unless you provide a polyfill. Node.js 22+ includes native EventSource. | | Build format | Ships both CJS (require()) and ESM (import). Your bundler picks the right one. |

API Key Security

  • On the backend, use a secret API key stored in environment variables.
  • On the frontend, use a public/read-only key that only has permission to evaluate flags, not manage them. Never expose your secret key in client-side code.

Single Instance Pattern

Create one FeatureFlagsClient instance and share it:

  • Backend: Create at app startup, dispose on SIGTERM / SIGINT.
  • React: Create outside the component tree, pass via <FeatureFlyProvider>.
  • Vue: Create before app.mount(), install via app.use(FeatureFlyPlugin, { client }).

Cache Strategy

| Environment | Recommended cacheTtlMs | Why | | ----------------- | ------------------------ | -------------------------------------------------------------------------- | | Backend (API) | 30_00060_000 | Flags change infrequently; caching reduces load on the flags API. | | Frontend (SPA) | 60_000120_000 | Combined with streaming: true, the cache is auto-invalidated on changes. | | Edge / Serverless | 0 (disabled) | Use edgeDocument for fully offline evaluation instead. |

Disposing Resources

Always call client.dispose() when you're done. This cleans up:

  • SSE connections
  • Cache timers
  • Metrics collectors
  • Event listeners
// Express / NestJS
process.on("SIGTERM", () => client.dispose());

// React
useEffect(() => () => client.dispose(), []);

// Vue
onUnmounted(() => client.dispose());

Node.js Version

Requires Node.js >= 18. For SSE streaming on the server, Node.js 22+ is recommended (native EventSource), or install a polyfill like eventsource for older versions.


⚙️ Evaluation Flow

When you call evaluateFlag(), the SDK follows this priority chain:

evaluateFlag('slug', context)
  │
  ├─ 1. Local Overrides   → instant return (no HTTP)
  ├─ 2. Edge Evaluator    → offline return if document loaded
  ├─ 3. Cache hit          → instant return (no HTTP)
  ├─ 4. Circuit Breaker    → reject if circuit is open
  ├─ 5. Retry w/ Backoff   → exponential backoff + jitter
  ├─ 6. HTTP Request       → GET /feature-flags/:slug/evaluate
  ├─ 7. Cache store        → persist result with TTL
  └─ 8. Fallback           → predefined defaults if everything fails

🔒 Resilience

Built-in, zero-config resilience. No plugins needed.

| Layer | What it does | | --------------------- | ----------------------------------------------------------------------- | | Retry | Retries failed requests with exponential backoff + jitter | | Circuit Breaker | Stops calling a failing API after N consecutive failures, auto-recovers | | Cache | Serves stale data while the API is down | | Fallback Defaults | Returns predefined safe values when nothing else works | | Local Overrides | Flags work completely offline | | Edge Evaluator | Full offline evaluation using a pre-fetched document |


📊 Feature Comparison

| Capability | FeatureFly | LaunchDarkly | Unleash | GrowthBook | Flagsmith | | ---------------------------------------- | :--------: | :-----------: | :-----------: | :--------: | :-------: | | Boolean flags | ✅ | ✅ | ✅ | ✅ | ✅ | | Multi-type values (string, number, JSON) | ✅ | ✅ | ✅ | ✅ | ✅ | | In-memory cache | ✅ | ✅ | ✅ | ✅ | ✅ | | Retry with backoff | ✅ | ✅ | ✅ | ✅ | ✅ | | Circuit breaker | ✅ | ✅ | ❌ | ❌ | ❌ | | Typed event system | ✅ | ⚠️ | ❌ | ❌ | ❌ | | Local overrides | ✅ | ⚠️ | ✅ | ✅ | ❌ | | Fallback defaults | ✅ | ✅ | ✅ | ✅ | ✅ | | Injectable logger | ✅ | ✅ | ✅ | ❌ | ❌ | | Dispose / cleanup | ✅ | ✅ | ✅ | ❌ | ❌ | | Workspace-level overrides | ✅ | ⚠️ | ❌ | ❌ | ✅ | | Streaming (SSE) | ✅ | ✅ | ✅ | ✅ | ✅ | | Targeting / segmentation | ✅ | ✅ | ✅ | ✅ | ✅ | | Percentage rollout | ✅ | ✅ | ✅ | ✅ | ✅ | | A/B testing | ✅ | ✅ | ❌ | ✅ | ✅ | | Edge evaluation (offline) | ✅ | ✅ | ✅ | ✅ | ❌ | | Impact metrics | ✅ | ✅ | ✅ | ✅ | ❌ | | React hooks | ✅ | ✅ | ✅ | ✅ | ✅ | | Vue composables | ✅ | ❌ | ❌ | ❌ | ❌ | | Self-hosted | ✅ | ❌ | ✅ | ✅ | ✅ | | TypeScript first | ✅ | ✅ | ⚠️ | ✅ | ⚠️ | | Open source | ✅ MIT | ✅ Apache 2.0 | ✅ Apache 2.0 | ✅ MIT | ✅ BSD-3 | | | | | | | | | Bundle size (gzipped) | ~11 KB core~22 KB full | ~100 KB+ | ~336 KB | ~9 KB | ~50 KB+ | | Runtime dependencies | 0 | 3+ | 5+ | 0 | 2+ | | Pricing | Free | Paid | Free/Paid | Free | Free/Paid |

✅ Supported · ⚠️ Partial · ❌ Not available


� Changelog

All notable changes to this project are documented in CHANGELOG.md.


��️ Roadmap

| Feature | Status | Description | | ------------------- | ------------ | -------------------------------------------- | | Multi-language SDKs | 🔜 Planned | Go, Python, Ruby, PHP server-side SDKs | | Encrypted payloads | 💡 Exploring | End-to-end encryption of flag configurations | | Audit log | 💡 Exploring | Track who changed what and when |


🧪 Testing

npm test           # Run tests with coverage
npm run test:watch # Watch mode

🤝 Contributing

  1. Clone the repo
  2. npm install
  3. npm test
  4. npm run build

📄 License

MIT © Arrua Platform Team

All notable changes to this project are documented in CHANGELOG.md.