OpenOps is a no-code FinOps automation platform for cloud cost optimization. Users build and execute workflows to automate cost-saving processes across AWS, Azure, and GCP, with a built-in spreadsheet-like database (OpenOps Tables) for structured data and collaboration.
Work from the repo root. Prefer small, scoped changes. Avoid unrelated refactors.
This is an Nx monorepo. Source code lives under packages/. In production we build two container images: App (UI + API) and Worker.
Key packages:
packages/server/api— Fastify API server; handles auth, flow management, app connections, and all REST endpoints (App container)packages/server/worker— workflow execution runtime; manages the engine process pool, job polling, and webhook handling (Worker container)packages/server/shared— shared server utilities: logging, caching, AES-256-CBC encryption (packages/server/shared/src/lib/security/encrypt-compress.ts)packages/engine— workflow execution engine; runs as isolated child processes spawned by the workerpackages/react-ui— React 18 frontend (Vite + TailwindCSS) (App container)packages/shared— shared types, error codes, and utilities across frontend and backendpackages/blocks/— 50+ composable integration blocks (AWS, Azure, GCP, Slack, etc.) built onpackages/blocks/frameworkpackages/ui-components— reusable component library (Storybook)packages/ui-kit— frontend tools, socket context, and API utilities
Other important files:
nx.json— Nx target defaults and caching config.env.template— template for local environment variables; copy to.envbefore running.github/workflows/ci.yml— source of truth for CI lint/test/build behavior.github/pull_request_template.md— required PR body format
- Backend: Fastify, TypeORM 0.3 (PostgreSQL or SQLite), JWT auth with secure cookie sessions
- Frontend: React 18, Zustand, react-query v5, shadcn, Axios (via
api.tswrapper),qsfor query strings - Testing: Jest
- Validation: AJV for JSON schema; typed
ApplicationErrorcodes mapped to HTTP status codes
- Node v24.16.0 (pinned in
.nvmrc) - Docker (for Postgres, Redis, OpenOps Tables, Analytics)
cp .env.template .env # fill in any missing values
npm ci --no-audit --no-fundarm64 / Apple Silicon:
.env.templatealready includesNODE_OPTIONS=--no-node-snapshot, required for theisolated-vmsandbox package.
npm start # installs deps, runs docker compose up --wait, then npm run devOr manually:
docker compose up -d --wait # start Postgres, Redis, OpenOps Tables, Analytics
npm run dev # start all services concurrentlyRunning services:
| Service | URL |
|---|---|
| Frontend (react-ui) | http://localhost:4200 |
| API (server/api) | http://localhost:3000 |
| Worker (server/worker) | http://localhost:3010 |
| OpenOps Tables | http://localhost:3001 |
| Analytics | http://localhost:8088 |
Default admin credentials: local-admin@openops.com / 12345678
- Identify the affected Nx project(s) before writing any code.
- Run focused, project-scoped commands while iterating.
- Before finalizing, run lint, tests, and build for all changed projects.
Project-scoped commands (preferred while iterating):
npx nx lint <project>
npx nx test <project>
npx nx build <project>Full monorepo commands (for final validation):
npx nx run-many --target=lint --quiet
npx nx run-many --target=test --quiet -- --silent
npx nx run-many --target=buildRoutes use Fastify + TypeBox schemas + RBAC. Pattern from packages/server/api/src/app/flows/:
import { getProjectScopedRoutePolicy } from '../core/security/route-policies/route-security-policy-factory';
import { Type } from '@sinclair/typebox';
const CreateThingOptions = {
config: {
security: getProjectScopedRoutePolicy({
allowedPrincipals: [PrincipalType.USER],
permission: Permission.WRITE_FLOW,
}),
},
schema: {
body: Type.Object({ name: Type.String() }),
response: { [StatusCodes.OK]: ThingSchema },
},
};
app.post('/', CreateThingOptions, async (request, reply) => {
// handler
});Register the controller in your module:
await app.register(thingController, { prefix: '/v1/things' });Every new route must have an explicit RBAC policy via getProjectScopedRoutePolicy().
npx nx db-migration server-api -- --name descriptive-nameThis generates a file in packages/server/api/src/app/database/migrations/ with a Unix timestamp prefix (e.g., 1776097737024-DescriptiveName.ts). Manually register it in postgres-connection.ts via getMigrations(). Migrations run automatically on startup when RUN_DB_MIGRATIONS is true.
Use the CLI generators from the repo root:
npm run create-block # scaffold a new block package
npm run create-action # add an action to an existing block
npm run create-trigger # add a trigger to an existing block
npm run sync-blocks # sync block metadataAction structure (packages/blocks/<name>/src/lib/actions/):
import { createAction, Property } from '@openops/blocks-framework';
export const myAction = createAction({
name: 'action_name',
displayName: 'Display Name',
description: '...',
isWriteAction: true,
auth: myAuth,
props: {
resourceId: Property.ShortText({
displayName: 'Resource ID',
required: true,
}),
},
async run(context) {
// implementation
},
});Trigger types: TriggerStrategy.POLLING, WEBHOOK, APP_WEBHOOK, SCHEDULED.
Pages live in packages/react-ui/src/app/routes/<page-name>/. Register in packages/react-ui/src/app/router.tsx:
{
path: 'things',
element: <ThingsPage />,
errorElement: <RouteErrorBoundary />,
}All user-facing strings must use i18n — no hardcoded copy.
Use ApplicationError with typed ErrorCode from @openops/shared:
import { ApplicationError, ErrorCode } from '@openops/shared';
try {
// throw
if (!entity) {
throw new ApplicationError({
code: ErrorCode.ENTITY_NOT_FOUND,
params: { entityType: 'Flow', entityId: id },
});
}
} catch (err) {
if (err instanceof ApplicationError && err.error.code === ErrorCode.ENTITY_NOT_FOUND) {
// handle
}
throw err;
}The Fastify error handler in packages/server/api/src/app/helper/error-handler.ts maps ErrorCode values to HTTP status codes automatically.
- All sensitive credentials must be encrypted via
encrypt-compress.tsinpackages/server/shared— never inline - Follow existing code style and patterns in the repository
- Write clear, self-documenting code with descriptive variable and function names
- Include comments for complex logic or non-obvious behavior
- Always write tests for new functionality and changes
- Update documentation for user-facing changes
- Do not introduce new dependencies without discussion
- Prefer the TypeORM repository pattern (
repoFactory) for data access; avoid raw SQL except in migrations or when there’s a clear performance need. - Every new API route needs an explicit RBAC policy via
getProjectScopedRoutePolicy() - Use the shared pagination helper for cursor-based pagination — do not implement ad-hoc pagination
- Workflow state is managed by
flow-run-servicevia an in-flight cache backed by the database - Keep API contract changes backward-compatible unless a migration is explicitly part of the change
Run "npx nx lint" to verify code style before committing.
- Indentation: 2 spaces (TypeScript/JavaScript, shell scripts)
- Line length: 100–120 characters preferred
- Braces: Required for all control blocks, even single-line
- Spacing:
- One space between keywords and parentheses:
if (condition) { - No trailing whitespace
- Newline at end of file
- One space between keywords and parentheses:
- Linting: Use ESLint as configured in each package
- Formatting: Follow Prettier rules if configured
- Respect
.editorconfig,.eslintrc,.prettierrc, and other config files
- Variables/functions:
camelCase - Classes/types:
PascalCase - Constants:
UPPER_SNAKE_CASE - Files: lowercase with hyphens (e.g.,
user-profile.ts)
- Use proper types and interfaces — avoid
any - Prefer
constoverlet; avoidvar - Explicit return types for all exported functions
- Prefer arrow functions for callbacks and functional components
- Explain why, not what — the code should be self-explanatory
- Use
TODO:for actionable technical debt - Document public functions, classes, and modules
- TDD — red-green-refactor. No production code without a failing test first.
- Place unit tests in a
tests/folder alongside the code. - Tests must assert meaningful behavior — avoid
toBeTruthy()ortoBeDefined()without further assertions.
Integration tests (in packages/server/api/test/integration/) initialize a real database:
beforeAll(async () => {
encryptUtils.loadEncryptionKey();
await databaseConnection().initialize();
});
afterAll(async () => databaseConnection().destroy());Unit tests (in packages/server/api/test/unit/) mock external dependencies via jest.mock(). Test helpers and fixtures live in packages/server/api/test/helpers/.
Run tests: Run tests using Nx commands
npx nx test <project> # all tests for a package
npx nx test-unit server-api # unit tests only for server-api
npx nx test ui-components # tests only for ui-components
npx nx test engine # tests only for engineCI runs one install job first, then the following jobs execute in parallel:
- audit —
npm audit - lint —
npx nx run-many --target=lint --quiet - check-licenses — validates dependency licenses
- test — runs tests in shards across packages
- build —
npx nx run-many --target=build
Mirror this locally when validating a change end-to-end.
NX_REJECT_UNKNOWN_LOCAL_CACHE=0 is not supported— remove from local env; conflicts with the newer Nx DB cache.- Vite deprecation warnings (
esbuildoption /optimizeDeps.rollupOptions) — warnings only, commands still succeed. - Jest ES module warnings (
Failed to load the ES module ... jest.config.ts) — tests still execute; treat as known noise. - Slow full-monorepo runs — use project-scoped Nx targets while iterating.
- Keep PRs single-purpose and scoped to the issue being addressed
- Only include changes related to the issue — no unrelated modifications
- Follow
.github/pull_request_template.md - Include testing notes: what was run, and what was intentionally scoped or skipped
- All PRs must reference a Linear issue in their body (e.g.,
Fixes OPS-100)
- Use imperative mood: "Fix bug" not "Fixed bug"
- Keep commits small and focused on a single change
- Describe what and why, not how
- Must start with a capital letter and a real word
- Must have at least three words
- Must use imperative mood (e.g., "Add GO support" not "Added GO support")
All PRs must reference a Linear issue in their body.
Examples:
- Fixes OPS-100.
- Resolves OPS-101.
- Part of CI-102.
- CONTRIBUTING.md - General contribution guidelines
- .github/pull_request_template.md - PR template
- .github/prlint.json - PR linting rules
- docs.openops.com - Official OpenOps documentation