Skip to main content

Manifest-Driven Gating

NearSync uses a SystemManifest -- a JSON document stored in the database -- as the single source of truth for what each organization can see and do. Module access, feature flags, branding, security policies, integration settings, and operational limits are all controlled by this document. Changes take effect in real time without code deployment.

Why a Manifest?

NearSync must support several instance configurations, 60+ feature flags, per-client branding (logo, colors, domain white-labeling), and module-level access control. Changing what a workspace has switched on should be a database record update, not a deployment.

Traditional approaches were evaluated and rejected:

ApproachWhy It Was Rejected
Environment variablesRequires redeployment for each flag change. Cannot upgrade a single client without redeploying.
Feature flag service (LaunchDarkly, etc.)Adds third-party dependency and cost. Overkill for simple on/off gating at this scale.
Database column per featureSchema changes for every new feature. DDL migrations required. Does not scale.

The SystemManifest approach stores all configuration in a single typed JSON document, loaded client-side with caching and realtime subscription.

SystemManifest Structure

The manifest contains seven top-level sections:

SystemManifest {
instance: {
id: string, // Organization identifier
tier: 'starter' | 'standard' | 'standard_plus' | 'advanced' | 'custom',
deployment: 'managed' | 'byok',
vertical: string, // Industry vertical
region: string, // Deployment region
trial: boolean,
trial_expires_at: string, // ISO 8601 timestamp
},

modules: {
[moduleId]: {
enabled: boolean,
tier_required: string, // Minimum tier for this module
feature_overrides: string[], // Features always-on for this module
}
// Module IDs: intelligence, dashboards, comms, sales, marketing,
// website, projects, operations, support, people, finance, system,
// bookmarks, user_settings
},

features: {
[featureFlagId]: boolean,
// 60+ flags: ai_chat_enabled, sms_campaigns_enabled,
// inventory_sync_enabled, multi_currency_enabled, etc.
},

security: {
mfa_required: boolean,
session_timeout_minutes: number,
ip_whitelist: string[], // CIDR blocks
password_policy: {
min_length: number,
require_uppercase: boolean,
require_symbols: boolean,
},
enforce_saml: boolean,
saml_provider_url: string,
},

branding: {
company_name: string,
logo_url: string,
favicon_url: string,
primary_color: string, // Hex color
surface_color: string, // Hex color
text_color: string, // Hex color
},

integrations: {
[provider]: {
enabled: boolean,
default_model: string, // For AI providers
// API keys are NEVER stored in the manifest.
// Keys are fetched at runtime from a separate encrypted store.
}
// Providers: whatsapp, twilio, gemini, openai, stripe,
// razorpay, and 10+ more
},

limits: {
max_users: number,
storage_gb: number,
ai_tokens_per_month: number,
crm_contacts_limit: number | null, // null = unlimited
},

operations: {
timezone: string, // IANA timezone
currency: string, // ISO 4217 code
date_format: string, // MM/DD/YYYY, DD/MM/YYYY, etc.
work_days: string[], // ['Mon', 'Tue', ...]
work_hours_start: string, // HH:mm
work_hours_end: string, // HH:mm
},
}

Security Note on API Keys

Integration API keys are never stored in the manifest. Key fields in the integrations section are intentionally null. Actual keys are stored in a separate integration_configs table, encrypted at rest, and only accessed server-side by Edge Functions. Client-side code never sees or transmits API keys.

Manifest Lifecycle

+-------------------------------------------------------------+
| global_settings Table (PostgreSQL) |
| category = 'system_manifest' |
| value = { instance, modules, features, ... } |
| updated_at tracks last change |
+-------------------------------------------------------------+
|
| AuthProvider mounts
v
+-------------------------------------------------------------+
| useSystemManifest() Hook |
| |
| 1. Check localStorage cache (keyed by org_id) |
| -> Cache hit? Return cached version |
| -> Cache miss? Continue |
| |
| 2. Create default manifest |
| -> Safe defaults for all fields |
| -> Ensures undefined fields do not crash the app |
| |
| 3. Fetch from database |
| -> Query global_settings table |
| -> Merge with defaults (defaults fill any missing fields)|
| |
| 4. Subscribe to realtime changes |
| -> Channel: global_settings |
| -> Events: INSERT, UPDATE, DELETE |
| -> On change: re-fetch, update localStorage, notify |
| |
| 5. Return typed SystemManifest |
+-------------------------------------------------------------+
|
| useAuth() exposes:
| { manifest, isModuleEnabled(), isFeatureEnabled() }
v
+-------------------------------------------------------------+
| Application Usage |
| |
| - ModuleGuard checks isModuleEnabled() |
| - Feature gates check isFeatureEnabled() |
| - Brand colors injected into CSS variables |
| - Limits enforced at form submission |
+-------------------------------------------------------------+

Realtime Updates

The manifest is subscribed to Supabase Realtime. When a support team member updates a client's tier or toggles a feature flag in the database, the change propagates to all open browser windows within 2-3 seconds. No page refresh needed.

Default Handling

When the manifest is loaded, it is merged with a set of safe defaults using createDefaultManifest(). This protects against missing fields when new flags are added to the codebase but an existing client's manifest has not been updated yet. Every feature flag defaults to false unless explicitly enabled.

Module Gating

ModuleGuard Component

The ModuleGuard component wraps entire routes and conditionally renders the child module or an upsell message:

<ModuleGuard moduleId="crm">
<SalesHQ />
</ModuleGuard>

If manifest.modules.crm.enabled is false, ModuleGuard renders an upgrade prompt instead of the Sales module. If true, the module renders normally.

All admin dashboard modules are wrapped with ModuleGuard in the router configuration.

Module Gating Flow

User signs up -> Module interest form
|
v
Trial period -> All modules enabled for evaluation
|
+-- User explores all modules
+-- Admin sees usage metrics per module
+-- Sales uses data to recommend tier
|
v
Conversion to paid -> Modules updated based on tier
|
v
Runtime gating:
ModuleGuard checks isModuleEnabled()
-> enabled: render module
-> disabled: render upgrade prompt

Tier-to-Module Mapping

The tier field on the manifest carries a default module set. Intelligence and AI are always available. The remaining modules are resolved from the tier default and can be overridden per workspace, so the defaults below are a starting point rather than a fixed entitlement.

For what a plan costs, see the pricing page.

TierAvailable ModulesSelection Model
Starter2Intelligence + AI always on. Pick up to 2 additional from a starter pool.
Standard4Intelligence + AI always on. Pick up to 4 from an expanded pool.
Standard Plus6Intelligence + AI always on. Pick up to 6 from most modules.
AdvancedAllIntelligence + AI always on. All remaining modules included.
CustomAllIntelligence + AI always on. All modules included, terms negotiable.

The TIER_MODULE_DEFAULTS mapping in @nearsync/types defines which modules are available at each tier. The manifest stores the resolved set of enabled modules per organization.

Feature Flags

Fine-grained boolean flags in manifest.features control individual subsystems within modules:

// Example feature flags
{
features: {
ai_chat_enabled: true,
ai_prds_enabled: true,
ai_column_mapping_enabled: true,
sms_campaigns_enabled: false,
email_sequences_enabled: true,
inventory_sync_enabled: false,
multi_currency_enabled: true,
saml_sso_enabled: false,
// ... 50+ more flags
}
}

Feature flags are checked in component code:

// Gate a small UI element
{manifest.features.ai_chat_enabled && <AIChat />}

// Gate an action
if (!manifest.features.multi_currency_enabled) {
showError("Multi-currency requires an upgrade");
}

Feature flags are per-organization and can be overridden individually. For example, one Advanced-tier customer might have SMS campaigns enabled while another does not.

Branding

The manifest's branding section drives the visual identity of each client's deployment:

branding: {
company_name: "Acme Corp",
logo_url: "https://storage.example.com/acme-logo.png",
favicon_url: "https://storage.example.com/acme-favicon.ico",
primary_color: "#6366F1",
surface_color: "#FFFFFF",
text_color: "#1A1A2E",
}

These values are injected into CSS variables by the ThemeProvider:

  • --accent-color receives primary_color
  • --app-surface receives surface_color
  • --app-background receives a computed background shade

All UI components reference these CSS variables, so branding changes propagate across the entire application instantly when the manifest is updated.

Limits Enforcement

The limits section defines resource caps per organization:

limits: {
max_users: 15,
storage_gb: 25,
ai_tokens_per_month: 200000,
crm_contacts_limit: 5000,
}

Limits are enforced at the application layer during form submissions and data creation operations. When a limit is reached, the UI displays an appropriate message and blocks the action.

BYOK deployments override max_users to unlimited since the client owns their infrastructure and absorbs the cost of additional users.

Security Policies

The manifest's security section controls authentication and access policies:

  • MFA enforcement - Require TOTP-based multi-factor authentication for all users
  • Session timeout - Configurable session duration in minutes
  • IP whitelisting - Restrict access to specific CIDR blocks
  • Password policy - Minimum length, uppercase requirements, symbol requirements
  • SAML enforcement - Require SAML SSO for enterprise identity providers

These policies are evaluated at login and during session validation.

Operational Defaults

The operations section stores region-specific defaults:

operations: {
timezone: 'America/New_York',
currency: 'USD',
date_format: 'MM/DD/YYYY',
work_days: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
work_hours_start: '09:00',
work_hours_end: '17:00',
}

These values are seeded based on the client's region at provisioning time and can be customized afterward. They affect date formatting, currency display, booking availability windows, and scheduling defaults across the platform.

Limitations and Future Work

Client-side gating only: ModuleGuard operates in the browser. A determined user could bypass it using browser DevTools and query the database directly. This is mitigated by RLS policies that enforce data access at the database level, but module-specific RLS (for example, denying SELECT on finance tables if the finance module is disabled) is planned as a future hardening step.

No audit trail (planned): Changes to the manifest are not currently tracked with who/when/why metadata. A manifest_audit_log table that records every change is planned for a future release.

Per-organization scope: The manifest is global per organization. Per-user feature flags (for example, enabling beta features for admins but not regular users) are not supported by the current design and would require a separate per-user flags table.