This document provides guidelines for AI coding agents working on the Ackee codebase.
Ackee is a self-hosted Node.js analytics tool built with:
- Backend: Node.js (ESM modules), Express, Apollo Server (GraphQL), Mongoose (MongoDB)
- Frontend: React (with createElement as
h), Apollo Client, SCSS - Build: Custom build script (
build.js), Rosid handlers - Testing: AVA test framework
- Code Quality: ESLint + Prettier (via @electerious configs)
npm run build # Production build
npm run build:pre # Development build (BUILD_ENV=pre)
npm start # Build and start servernpm run dev # Start with nodemon (auto-rebuild + restart)
npm run server # Start server without buildingnpm test # Run lint + all tests
npm run lint # ESLint + Prettier check only
ava # Run all tests without linting
ava test/path/to/file.js # Run a single test file
ava test/**/*domains*.js # Run tests matching pattern
ava --watch # Run in watch modenpm run eslint # Check JavaScript with ESLint
npm run prettier -- --check # Check formatting
npm run format # Auto-fix ESLint + Prettier issuesnpm run healthcheck # Run health check script- Use ES modules (
.jsfiles withtype: "module"in package.json) - No TypeScript - pure JavaScript with JSDoc comments where needed
- Functional programming style preferred
- Keep code simple, readable, and minimal
- Use
.jsextensions in all import paths - Group imports logically: external deps → internal modules → utils
- Use named exports for utilities, default exports for main components/resolvers
import { randomUUID as uuid } from 'node:crypto'
import Domain from '../models/Domain.js'
import sortByProp from '../utils/sortByProp.js'- Backend: camelCase for files (e.g.,
domains.js,requireAuth.js) - Frontend Components: PascalCase (e.g.,
Input.js,Dashboard.js) - Frontend Hooks: camelCase with
useprefix (e.g.,useDomains.js) - Constants: camelCase files (e.g.,
routes.js,intervals.js)
- Uses Prettier via
@electerious/prettier-config - Tabs for indentation (configured in Prettier)
- Single quotes for strings
- Trailing commas in multi-line structures
- Do not manually format - run
npm run formatinstead
- Use
createElement as hinstead of JSX - Define PropTypes for all components
- Use functional components with hooks
- Custom hooks follow
use*naming convention
import { createElement as h } from 'react'
import PropTypes from 'prop-types'
const Component = (props) => {
return h('div', { className: 'example' }, props.children)
}
Component.propTypes = {
children: PropTypes.node,
}
export default Component- Use
gqltemplate tag from@apollo/client - Define fragments in separate files
- Mutations return
{ success, payload }structure
const QUERY = gql`
query fetchDomains {
domains {
...domainFields
}
}
${domainFields}
`- Use
KnownErrorclass for user-facing errors - Catch and transform ValidationErrors from Mongoose
- Always handle promise rejections
- Use
signalefor logging (notconsole.log)
try {
entry = await domains.add(input)
} catch (error) {
if (error.name === 'ValidationError') {
throw new KnownError(messages(error.errors))
}
throw error
}- Resolvers use
pipe()utility to compose middleware - Common middleware:
requireAuth,blockDemoMode
createDomain: pipe(requireAuth, blockDemoMode, async (parent, { input }) => {
const entry = await domains.add(input)
return { payload: entry, success: true }
})- Export named functions for CRUD operations
- Use
response()transformer to shape data - Use
enhance()pattern for consistent transformations
export const get = async (id) => {
const enhance = (entry) => {
return entry == null ? entry : response(entry)
}
return enhance(await Domain.findOne({ id }))
}- Use
test.serial()for tests that depend on execution order - Import AVA as
test from 'ava' - Use
test.before,test.after.always,test.beforeEach,test.afterEach.always - Test context (
t.context) stores shared state (e.g., tokens) - Organize tests in folders matching source structure
import test from 'ava'
import { api, cleanup, fillDatabase, gql } from './_utils.js'
test.beforeEach(fillDatabase)
test.afterEach.always(cleanupDatabase)
test.serial('create domain', async (t) => {
const { json } = await api(base, body, t.context.token.id)
t.true(json.data.createDomain.success)
t.is(json.data.createDomain.payload.title, expectedTitle)
})Base config: @electerious/eslint-config
Disabled rules for this project:
import-x/dynamic-import-chunknameimport-x/no-named-as-defaultunicorn/filename-caseunicorn/consistent-function-scopingunicorn/no-await-expression-memberunicorn/no-anonymous-default-exportunicorn/prefer-top-level-awaitunicorn/no-thenableunicorn/no-process-exit
src/
├── aggregations/ # Data aggregation functions
├── constants/ # Shared constants and enums
├── database/ # Database CRUD operations
├── middlewares/ # GraphQL middleware (auth, demo mode)
├── models/ # Mongoose models
├── resolvers/ # GraphQL resolvers
├── stages/ # Pipeline stages
├── types/ # GraphQL type definitions
├── ui/ # React frontend
│ ├── scripts/ # React components, hooks, utils
│ └── styles/ # SCSS stylesheets
└── utils/ # Utility functions
test/
├── aggregations/ # Aggregation tests
├── constants/ # Constants tests
├── resolvers/ # Resolver tests
└── utils/ # Utility tests
- Node.js version: Requires Node.js >= 24
- Environment variables: Uses
.envfiles (see.envfor local config) - MongoDB: Required for development and testing (uses mongodb-memory-server for tests)
- Development mode: Set
NODE_ENV=developmentfor GraphQL Playground access - Demo mode: Set
ACKEE_DEMO=trueto enable demo mode (blocks mutations) - Contributing: Always work on
developbranch, discuss changes in issues first