Skip to content

Cursor

Cursor is an AI-powered code editor that helps you write code. A fork of VSCode.

We have created a .cursorrules file in the root of the repository to help your AI Agent assist you with developing services that utilize Flowcore.

You can read more about Cursor Rules here.

This file also has styling, formatting and commit message rules that are based on how we write code at Flowcore.

# <project-name>
Every time you choose to apply a rule(s), explicitly state the rule(s) in the output. You can abbreviate the rule description to a single word or phrase.
## Project Context
<project-description>
- <project-feature-1>
- <project-feature-2>
- <project-feature-3>
## Flowcore Platform Concepts
This project is built to utilize the Flowcore Platform, and as such, it is important to understand the concepts of the Flowcore Platform.
- When writing events to a database it is done through sending the event through a webhook (using the webhook builder) via the Flowcore Platform.
- The events come (from the Flowcore Platform) to the transformer endpoints in the form of a JSON object.
- The transformer endpoints follow the convention of `/transformers/<flow-type-name>/route.ts`.
- Each event type is split into a different transformer file, for example the create, update and delete events for the `tenant.0` event type are split into `tenant.0/tenant-created.ts`, `tenant.0/tenant-updated.ts` and `tenant.0/tenant-deleted.ts` respectively then added to the `tenant.0/route.ts` file.
- The transformer endpoints are responsible for writing the events to the database.
- The api routes under the `src/server/routes/api/v1/` directory are responsible for sending the events to the Flowcore Platform.
- The api can listen to other non-local events, generated by other services using the Flowcore Platform, these are also routed to the transformer endpoints.
- The events that are used conform to contracts defined in the `src/contracts/events/` directory.
example of a contract:
\`\`\`typescript
// Contract
export const dataCoreV1Contract = {
flowType: "data-core.1",
eventTypes: {
created: "data-core.created.0",
updated: "data-core.updated.0",
deleted: "data-core.deleted.0",
},
} as const satisfies FlowcoreEventContract
export const EventDataCoreV1CreatedSchema = <@sinclair/typebox schema>
export const EventDataCoreV1UpdatedSchema = <@sinclair/typebox schema>
export const EventDataCoreV1DeletedSchema = <@sinclair/typebox schema>
// Types
export type EventDataCoreV1Created = Static<typeof EventDataCoreV1CreatedSchema>
export type EventDataCoreV1Updated = Static<typeof EventDataCoreV1UpdatedSchema>
export type EventDataCoreV1Deleted = Static<typeof EventDataCoreV1DeletedSchema>
\`\`\`
- The webhooks and transformers use the @flowcore/sdk-transformer-core package for the builders.
example of a webhook builder:
\`\`\`typescript
// WebhookBuilder is imported from @flowcore/sdk-transformer-core
export const webhookBuilder = new WebhookBuilder({
baseUrl: env.FLOWCORE_WEBHOOK_BASE_URL,
tenant: env.FLOWCORE_TENANT,
dataCore: env.FLOWCORE_DATA_CORE,
apiKey: env.FLOWCORE_API_KEY,
}).withPredicate({
predicate: redisPredicate.check,
options: {
maxAttempts: 20,
attemptDelayMs: 250,
},
})
// using the builder to create a webhook
export const webhookTenantCreated = webhookBuilder.buildWebhook<EventTenantCreated>(
tenantContract.flowType,
tenantContract.eventTypes.created,
)
// using the webhook to send an event
await webhookTenantCreated.send(event)
\`\`\`
- if events are processed elsewhere, then there is no need to have a predicate on the webhook. It is best practice to have a webhook with no predicate and one with a predicate for ease of use.
example of a elysia transformer factory:
\`\`\`typescript
class ElysiaTransformerBuilder extends TransformerBuilder<typeof FlowcoreEventSchema> {
public getRouter(prefix?: string) {
return new Elysia().post(
`/${prefix ?? ""}${this.flowType}`,
async ({ body, headers, set }) => {
const response = await this.handleEvent(body, headers["x-secret"])
set.status = response.statusCode
return response
},
{
body: FlowcoreEventSchema,
headers: TransformerHeadersSchema,
response: TransformerResponseSchema,
},
)
}
}
type ConstructedTransformerFactory = (flowType: string) => ElysiaTransformerBuilder
type TransformerOptions = {
onSuccess?: TransformerSuccessHandler
secret: string
}
const transformerFactory = (options: TransformerOptions): ConstructedTransformerFactory => {
return (flowType: string) => {
const builder = new ElysiaTransformerBuilder(flowType).withSecret(options.secret)
if (options.onSuccess) {
builder.withSuccessHandler(options.onSuccess)
}
return builder
}
}
export const transformerBuilder = transformerFactory({
onSuccess: redisPredicate.notify,
secret: env.TRANSFORMER_SECRET,
})
// using the transformer factory to create a transformer router
export default transformerBuilder(organizationV0Contract.flowType)
.onEventType(organizationV0Contract.eventTypes.created, EventOrganizationV0CreatedSchema, organizationCreatedHandler)
.onEventType(organizationV0Contract.eventTypes.updated, EventOrganizationV0UpdatedSchema, organizationUpdatedHandler)
.onEventType(organizationV0Contract.eventTypes.deleted, EventOrganizationV0DeletedSchema, organizationDeletedHandler)
.getRouter()
\`\`\`
- use a webhook fixture for testing the webhook builder
## Flowcore Platform SDK
When using the Flowcore Platform SDK, use the `@flowcore/sdk` package.
- always check if the command you are using is already defined in the `@flowcore/sdk` package.
- if no command is found, create a new command in the `src/contracts/flowcore-sdk/` directory.
- when creating a new command always check the api swagger documentation get get the correct path, method and response schema.
- swagger docs can be found at <https://<api url>/swagger/json>
- the Command base class from the `@flowcore/sdk` package handles the authentication
expample command:
\`\`\`typescript
import { Command, parseResponseHelper } from "@flowcore/sdk";
import { Type } from "@sinclair/typebox";
export type TenantEntity = {
/** The tenant id */
id: string;
/** The tenant name */
name: string;
/** The tenant display name */
displayName: string | null;
/** The tenant description */
description: string | null;
/** The tenant website */
websiteUrl: string | null;
};
const TenantSchema = Type.Object({
id: Type.String(),
name: Type.String(),
displayName: Type.Union([Type.String(), Type.Null()]),
description: Type.Union([Type.String(), Type.Null()]),
websiteUrl: Type.Union([Type.String(), Type.Null()]),
});
/**
* Fetch a list of tenants
*/
export class TenantListCommand extends Command<unknown, TenantEntity[]> {
/**
* Get the method
*/
protected override getMethod(): string {
return "GET";
}
/**
* Get the base url
*/
protected override getBaseUrl(): string {
return "https://tenant.api.flowcore.io";
}
/**
* Get the path
*/
protected override getPath(): string {
return "/api/v1/tenants";
}
/**
* Parse the response
*/
protected override parseResponse(rawResponse: unknown): TenantEntity[] {
const response = parseResponseHelper(Type.Array(TenantSchema), rawResponse);
return response;
}
}
\`\`\`
## Flowcore Platform Deployment
This service is deployed to the Flowcore Platform, and as such, it is important to understand the primitives (<https://docs.flowcore.io/guides/architecture/primitives/>).
This is defined in a few Flowcore YAML files:
- `flowcore.yaml` - defines the flowcore platform primitives for production
- `flowcore.local.yaml` - defines the flowcore platform primitives for local development
For local development, the configuration is stored in the `flowcore.deployment.json` file.
- `flowcore.deployment.json` - defines the flowcore platform primitives for streaming flowcore events to the transformers
this project uses the concept of proxy transformers, so a proxy transformer needs to be created for each flow type that is being used, the configuration for the proxy transformer is stored under the scenario in the `flowcore.yml` file.
## Code Style and Structure
- Write concise, technical TypeScript code.
- Use functional and declarative programming patterns; avoid classes.
- Prefer iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., isLoaded, hasError).
- Use Flowcore's documentation and examples as a reference for the Flowcore Platform. (full index at <https://docs.flowcore.io/automation/indexing/>)
- Check the codebase for existing usage of the feature you are implementing before starting the implementation.
- Structure repository files as follows:
Server
src/ # Source code root directory
├── contracts/ # TypeScript type definitions, interfaces, and API contracts
│ ├── flowcore-sdk/ # Flowcore Platform SDK commands and types
│ ├── common/ # Common types and interfaces, generated or statically defined
│ └── events/ # Flowcore Platform Event contracts
├── database/ # Drizzle Database models/schemas
├── env/ # Environment variables and configuration
├── lib/ # Shared libraries and helper utility functions
├── server/ # Elysia Server application core
│ ├── guards/ # Authentication and authorization middleware
│ ├── providers/ # Stores and other providers
│ ├── routes/ # API route definitions and handlers
│ ├── api/ # API route grouping and versioning
│ │ └── v1/ # Version 1 API implementation
│ │ └── tenants/ # Tenant management endpoints
│ │ ├── configuration/ # Configuration management
│ │ ├── license/ # License-related operations
│ │ ├── event-type-subscription/ # Event subscription management
│ └── health.ts # Health check endpoint for monitoring
├── services/ # Business logic and service layer implementations
├── transformers/ # Flowcore Platform transformers and transformer builders
│ ├── data-core.1/ # Event transformers for the data-core.1 events
│ ├── event-type.1/ # Event transformers for the event-type.1 events
│ ├── flow-type.1/ # Event transformers for the flow-type.1 events
│ ├── tenant.0/ # Event transformers for the tenant.0 events
│ └── organization.0/ # Event transformers for the organization.0 events
└── webhooks/ # Webhook builders and factories
Tests
test/ # Test suite directory
├── data/ # Test data folder, containing static data for testing
├── fixtures/ # Test fixtures and dynamic data generation used for testing
├── utils/ # Test utilities and helper functions
└── tests/ # Test files for each logical route/tags in the API
└── api/ # Test files for each API route
## Tech Stack
- TypeScript
- Bun
- ElysiaJS
- DrizzleJS
- AWS Secrets Manager
- Flowcore Platform
- Docker
- Github Actions
## Naming Conventions
- use lowercase with dashes for directory names and file names (e.g. `src/server/routes/api/v1/tenants/event-type-subscription/`)
- favour named exports for components and utilities
## TypeScript Usage
- Use TypeScript for all code; prefer interfaces over types.
- Avoid enums; use objects or maps instead.
- Avoid using `any` or `unknown` unless absolutely necessary. Look for type definitions in the codebase instead.
- Avoid type assertions with `as` or `!`.
- Use strict mode in TypeScript for better type safety.
- use aliases for imports where possible, for example `import {} from "../../../src/server/routes/api/v1/tenants/metrics/metrics.post.ts"` can be aliased as `import metricsPost from "@/server/routes/api/v1/tenants/metrics/metrics.post.ts"` look for aliases in the `tsconfig.json` file.
- look for types and imports in local files before using external libraries.
## Syntax and Formatting
- Use the "function" keyword for pure functions.
- Use the "const" keyword for all variables where possible.
- Use double quotes over single quotes for strings.
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
- Use Biome for code formatting and linting.
## Error Handling and Validation
- Use @sinclair/typebox for runtime validation and error handling.
- Prioritize error handling and edge cases:
- Handle errors at the beginning of functions.
- Use early returns for error conditions to avoid deeply nested if statements.
- Avoid unnecessary else statements; use if-return pattern instead.
- Implement global error boundaries to catch and handle unexpected errors.
## Testing
- only test the public API of the service, not the internal implementation details.
- treat the service as a black box, only testing the public API.
- prefer end-to-end tests over unit tests, only write unit tests for critical internal functionality.
- Use Bun's test framework for testing.
- the test setup is in the `test/setup.ts` file.
- prefer to use docker containers and docker compose in the test setup.
- prefer to create a fixture for simple serving of endpoints, for example a simple service for validating if a webhook has been sent correctly would be created as a "webhook.fixture.ts" file.
- use fakerjs for generating realistic test data.
- ensure that each public API endpoint has a test.
- look for fixture files before creating a new ones, for example if there is a fixture for a webhook, then use/augment that instead of creating a new one.
- validate that the tests work with the `bun test <test-file-name> --bail` command.
- look for existing configuration in the `test/setup.ts` file before intializing new fixtures in the test file, there might already be a fixture for the feature you are testing, and maybe the setup file is the correct place to configure the fixture or amend the existing fixture.
- always suggest to run the test with the `bun test <test-file-name> --bail` command after writing a test.
## ElysiaJS
- add descriptions and tags to each route.
- add return types and descriptions to each possible response defined in the code.
- add examples to each schema so that it will be generated in the swagger documentation.
- remember to add the endpoint to the nearest `route.ts` file.
- check other endpoints in the nearest `route.ts` file to see how they are defined.
- use the `@sinclair/typebox` package for validation and error handling.
## Database
- run `NODE_ENV=test bun test/fixtures/db.fixture.ts action:up` after changing the database schema.
## Commit Messages
- use chore for changes to the cursor rules file
- follow this template for commit messages: `<type>(<scope>): :emoji: <subject>`
- prefer no description in the commit message, only add description if it is not already obvious by the subject what the commit is doing
- use an emoji in commit messages to make them more readable (eg. :sparkles: for new features, :bug: for bug fixes, :memo: for documentation, :fire: for removing code or files, :art: for code formatting, :rocket: for deploying, :tada: for new features, :memo: for documentation, :bug: for bug fixes, :fire: for removing code or files, :rocket: for deploying, :tada: for new features, :memo: for documentation, :bug: for bug fixes, :fire: for removing code or files, :rocket: for deploying, :tada: for initial commits)
- feature commits should be prefixed with "feat:"
- bug fixes should be prefixed with "fix:"
- documentation should be prefixed with "docs:"
- refactoring should be prefixed with "refactor:"
- chore should be prefixed with "chore:"
- formatting should be prefixed with "format:"