Feature Flags
ForgeX uses feature flags to control which modules, roles, and features are enabled across the platform. This allows safe rollout of new functionality, gradual phase releases, and environment-specific configurations without code changes.
Feature flags are defined in packages/shared/constants/features.js and shared across all services for consistent behavior.
Why Feature Flags?β
Phased Rollout
Roll out Phase 2 (Projects) and Phase 3 (Field) features gradually without affecting Phase 1 (Bids) users.
Environment Control
Enable features in dev/staging before production. Test new features safely.
Role Management
Hide roles (OPS, ACCOUNTING, FOREMAN) until their corresponding modules are ready.
Zero Downtime
Toggle features without deployments or server restarts.
Feature Flags Configurationβ
All feature flags are defined in packages/shared/constants/features.js:
module.exports = {
// Module availability flags
ENABLED_MODULES: {
BID_MANAGER: true, // Phase 1 - LIVE β
PROJECT_MANAGEMENT: false, // Phase 2 - Coming soon π§
FIELD_OPERATIONS: false, // Phase 3 - Coming soon π§
},
// Role visibility flags
ENABLED_ROLES: {
ADMIN: true, // Phase 1 β
ESTIMATOR: true, // Phase 1 β
PM: true, // Phase 1 (read-only bids) β
OPS: false, // Phase 2 π§
ACCOUNTING: false,// Phase 2 π§
FOREMAN: false, // Phase 3 π§
},
// Service API endpoints (environment-specific)
API_ENDPOINTS: {
BIDS: process.env.BIDS_API_URL || 'http://localhost:5001',
PROJECTS: process.env.PROJECTS_API_URL || 'http://localhost:5002',
FIELD: process.env.FIELD_API_URL || 'http://localhost:5003',
},
// Pub/Sub topic names
PUBSUB_TOPICS: {
BID_AWARDED: 'bid-awarded',
PROJECT_UPDATED: 'project-updated',
TIMESHEET_SUBMITTED: 'timesheet-submitted',
}
};
Module Flagsβ
Module flags control which apps/services are available to users.
ENABLED_MODULESβ
| Flag | Status | Description |
|---|---|---|
| BID_MANAGER | β
true | Phase 1 - Bid management system (live) |
| PROJECT_MANAGEMENT | π§ false | Phase 2 - PO tracking, cost management |
| FIELD_OPERATIONS | π§ false | Phase 3 - Timesheets, GPS tracking |
How Module Flags Workβ
- Portal (App Tiles)
- Backend (API Guards)
- Frontend (Navigation)
Portal Service reads ENABLED_MODULES to show/hide app tiles:
import { ENABLED_MODULES } from '../../../../packages/shared/constants/features';
const availableApps = [
{ name: 'Bids', enabled: ENABLED_MODULES.BID_MANAGER, url: '/bids' },
{ name: 'Projects', enabled: ENABLED_MODULES.PROJECT_MANAGEMENT, url: '/projects' },
{ name: 'Field', enabled: ENABLED_MODULES.FIELD_OPERATIONS, url: '/field' }
];
// Only show enabled apps
const visibleApps = availableApps.filter(app => app.enabled);
Result:
- User sees only the Bids tile (Phase 1)
- Projects and Field tiles are hidden until flags are enabled
Backend services check flags before allowing access:
const { ENABLED_MODULES } = require('../../../../packages/shared/constants/features');
// Middleware: Block access to disabled modules
function requireModuleEnabled(module) {
return (req, res, next) => {
if (!ENABLED_MODULES[module]) {
return res.status(503).json({
error: 'Service unavailable',
message: `${module} is not yet enabled`
});
}
next();
};
}
// Apply to routes
app.use('/api/projects', requireModuleEnabled('PROJECT_MANAGEMENT'));
app.use('/api/field', requireModuleEnabled('FIELD_OPERATIONS'));
Result:
- API returns 503 if module is disabled
- Prevents premature access to unreleased features
Frontend apps hide navigation links for disabled modules:
import { ENABLED_MODULES } from '../../../../packages/shared/constants/features';
const NavBar = () => (
<nav>
<Link to="/bids">Bids</Link>
{ENABLED_MODULES.PROJECT_MANAGEMENT && (
<Link to="/projects">Projects</Link>
)}
{ENABLED_MODULES.FIELD_OPERATIONS && (
<Link to="/field">Field</Link>
)}
</nav>
);
Result:
- Only Bids link is visible (Phase 1)
- Projects/Field links appear when flags are enabled
Role Flagsβ
Role flags control which user roles are visible in the UI (user management, dropdowns, etc.).
ENABLED_ROLESβ
| Flag | Status | Used By | Description |
|---|---|---|---|
| ADMIN | β
true | All modules | Full platform access |
| ESTIMATOR | β
true | Bids | Create/edit bids |
| PM | β
true | Bids (read), Projects | Project management |
| OPS | π§ false | Projects | Purchase orders |
| ACCOUNTING | π§ false | Projects | Cost reconciliation |
| FOREMAN | π§ false | Field | Crew management |
All roles exist in the database (User model includes all 6 roles). Role flags only control UI visibility, not database schema.
How Role Flags Workβ
- User Management UI
- Backend Validation
- Permission Checks
Admin panel filters role dropdown to show only enabled roles:
import { ENABLED_ROLES } from '../../../../packages/shared/constants/features';
import { UserRole } from '../../../../packages/shared/types';
const allRoles = Object.values(UserRole);
const availableRoles = allRoles.filter(role => ENABLED_ROLES[role]);
// Render dropdown
<Select options={availableRoles} />
Result:
- Dropdown shows: ADMIN, ESTIMATOR, PM
- Hidden: OPS, ACCOUNTING, FOREMAN (until Phase 2/3)
Backend validates roles against enabled flags:
const { ENABLED_ROLES } = require('../../../../packages/shared/constants/features');
function validateRole(role) {
if (!ENABLED_ROLES[role]) {
throw new Error(`Role ${role} is not yet available`);
}
}
// On user creation/update
app.post('/api/users', (req, res) => {
const { role } = req.body;
validateRole(role); // Throws if role is disabled
// Proceed with user creation
});
Result:
- API rejects attempts to assign disabled roles
- Prevents premature role assignment
Permission checks still work for all roles (even disabled ones):
function requireRole(allowedRoles) {
return (req, res, next) => {
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// OPS can still access if they have the role (even if flag is off)
app.post('/api/purchase-orders', requireRole(['ADMIN', 'OPS']));
Why? Role flags hide roles from UI, but don't disable functionality. This allows:
- Testing new roles in dev before production
- Early access for specific users
API Endpoints Configurationβ
API_ENDPOINTS defines service URLs for inter-service communication and frontend API calls.
Default Valuesβ
API_ENDPOINTS: {
BIDS: process.env.BIDS_API_URL || 'http://localhost:5001',
PROJECTS: process.env.PROJECTS_API_URL || 'http://localhost:5002',
FIELD: process.env.FIELD_API_URL || 'http://localhost:5003',
}
Environment-Specific Overridesβ
- Local Development
- Staging
- Production
.env (local):
BIDS_API_URL=http://localhost:5001
PROJECTS_API_URL=http://localhost:5002
FIELD_API_URL=http://localhost:5003
Services communicate via localhost ports.
.env (staging):
BIDS_API_URL=https://bids-staging.precisionsiteservices.com
PROJECTS_API_URL=https://projects-staging.precisionsiteservices.com
FIELD_API_URL=https://field-staging.precisionsiteservices.com
Services communicate via staging subdomains.
.env (production):
BIDS_API_URL=https://bids.precisionsiteservices.com
PROJECTS_API_URL=https://projects.precisionsiteservices.com
FIELD_API_URL=https://field.precisionsiteservices.com
Services communicate via production subdomains.
Usageβ
import { API_ENDPOINTS } from '../../../../packages/shared/constants/features';
// Frontend: Make API call to correct environment
const response = await fetch(`${API_ENDPOINTS.BIDS}/api/bids`, {
method: 'GET',
credentials: 'include' // Sends session cookies
});
// Backend: Call another service
const projectsResponse = await axios.post(`${API_ENDPOINTS.PROJECTS}/api/projects`, data);
Pub/Sub Topicsβ
PUBSUB_TOPICS defines Google Cloud Pub/Sub topic names for inter-service events.
Topic Definitionsβ
PUBSUB_TOPICS: {
BID_AWARDED: 'bid-awarded',
PROJECT_UPDATED: 'project-updated',
TIMESHEET_SUBMITTED: 'timesheet-submitted',
}
Event Flow (Phase 2+)β
Usage (Bids Service):
const { PUBSUB_TOPICS } = require('../../../../packages/shared/constants/features');
const { PubSub } = require('@google-cloud/pubsub');
const pubsub = new PubSub();
// Publish event when bid is awarded
async function onBidAwarded(bid) {
const topic = pubsub.topic(PUBSUB_TOPICS.BID_AWARDED);
await topic.publishMessage({
json: {
bidId: bid.id,
jobName: bid.jobName,
clientId: bid.clientId,
totalCost: bid.totalCost,
awardedAt: new Date().toISOString()
}
});
}
Usage (Projects Service):
// Subscribe to bid awarded events
const subscription = pubsub.subscription('projects-service-sub');
subscription.on('message', async (message) => {
const data = JSON.parse(message.data.toString());
if (message.attributes.topic === PUBSUB_TOPICS.BID_AWARDED) {
await createProjectFromBid(data);
}
message.ack();
});
Enabling New Featuresβ
Edit packages/shared/constants/features.js:
ENABLED_MODULES: {
BID_MANAGER: true,
PROJECT_MANAGEMENT: true, // β Enable Phase 2
FIELD_OPERATIONS: false,
}
Local development:
docker-compose restart
Production:
- Redeploy services that import
features.js - Or use environment variables for dynamic control (see below)
Portal: Projects tile should now be visible
API: /api/projects endpoints should return 200 (not 503)
Frontend: Projects navigation link should appear
If the module requires new roles, enable them:
ENABLED_ROLES: {
ADMIN: true,
ESTIMATOR: true,
PM: true,
OPS: true, // β Enable for Projects module
ACCOUNTING: true, // β Enable for Projects module
FOREMAN: false,
}
Environment-Based Feature Flagsβ
For runtime control without code changes, use environment variables:
Setupβ
Modify features.js to read from env:
module.exports = {
ENABLED_MODULES: {
BID_MANAGER: process.env.ENABLE_BID_MANAGER !== 'false', // Default: true
PROJECT_MANAGEMENT: process.env.ENABLE_PROJECT_MANAGEMENT === 'true', // Default: false
FIELD_OPERATIONS: process.env.ENABLE_FIELD_OPERATIONS === 'true', // Default: false
},
ENABLED_ROLES: {
ADMIN: true,
ESTIMATOR: true,
PM: true,
OPS: process.env.ENABLE_OPS_ROLE === 'true',
ACCOUNTING: process.env.ENABLE_ACCOUNTING_ROLE === 'true',
FOREMAN: process.env.ENABLE_FOREMAN_ROLE === 'true',
}
};
Usageβ
.env (staging):
ENABLE_PROJECT_MANAGEMENT=true
ENABLE_OPS_ROLE=true
ENABLE_ACCOUNTING_ROLE=true
.env (production):
# Keep Phase 2 disabled in prod until fully tested
ENABLE_PROJECT_MANAGEMENT=false
ENABLE_OPS_ROLE=false
ENABLE_ACCOUNTING_ROLE=false
Result:
- Staging has Projects module enabled for testing
- Production keeps it disabled until release
- No code changes needed β just env variable toggle
Best Practicesβ
Test in Staging First
Enable new features in staging before production. Verify functionality thoroughly.
Document Flag Changes
Update this documentation when adding or changing flags. Include release notes.
Use Environment Variables
For runtime control, use env vars instead of hardcoded booleans.
Coordinate Deploys
When enabling multi-service features, deploy all affected services together.
Common Scenariosβ
Enabling Phase 2 (Projects Module)
Steps:
- Set
ENABLED_MODULES.PROJECT_MANAGEMENT = true - Set
ENABLED_ROLES.OPS = true - Set
ENABLED_ROLES.ACCOUNTING = true - Deploy all services (portal, bids, projects)
- Verify Projects tile appears in Portal
- Test Projects API endpoints
Testing New Features in Dev
Steps:
- Enable flag in local
.envfile - Restart Docker Compose:
docker-compose restart - Test feature thoroughly
- If stable, enable in staging
- After staging verification, enable in production
Gradual Role Rollout
Scenario: Enable OPS role only for specific users before full release.
Steps:
- Keep
ENABLED_ROLES.OPS = false(hides from UI) - Manually assign OPS role to test users via database
- Test users can access OPS features (permissions still work)
- After validation, set
ENABLED_ROLES.OPS = truefor all
Hotfix: Disable Buggy Feature
Scenario: Production feature has a critical bug. Disable immediately.
Steps (if using env vars):
- Update production
.env:ENABLE_PROJECT_MANAGEMENT=false - Restart services:
gcloud run services update projects-backend - Feature is disabled until hotfix is deployed
Steps (if hardcoded):
- Edit
features.js: Set flag tofalse - Commit and deploy emergency patch
- Re-enable after bug is fixed
Feature Flag Historyβ
| Date | Change | Reason |
|---|---|---|
| Jan 2025 | BID_MANAGER: true | Phase 1 production launch |
| Jan 2025 | ENABLED_ROLES.ADMIN/ESTIMATOR/PM: true | Phase 1 roles enabled |
| TBD | PROJECT_MANAGEMENT: true | Phase 2 release |
| TBD | ENABLED_ROLES.OPS/ACCOUNTING: true | Phase 2 roles enabled |
| TBD | FIELD_OPERATIONS: true | Phase 3 release |
| TBD | ENABLED_ROLES.FOREMAN: true | Phase 3 roles enabled |
Future Enhancementsβ
Planned improvements for feature flag system:
- Admin UI for flags: Toggle features from admin panel (no code changes)
- Per-user flags: Enable features for specific users (beta testing)
- Time-based flags: Auto-enable features on a scheduled date
- A/B testing: Enable features for 50% of users
- Feature analytics: Track usage of flagged features