Contributing
Thanks for contributing to Comma. This document covers the conventions, patterns, and process.
Code style
TypeScript
- Strict mode. No
any. TypeScript is set tostrict: trueintsconfig.json. If you can’t avoid a dynamic type, useunknownand narrow it. - No type assertions without justification.
as SomeTypeis acceptable when you’re feeding output from a runtime source (e.g. SQLite results). Document why if it’s non-obvious. - Prefer
typeoverinterfacefor data shapes. Useinterfaceonly when you need declaration merging.
Database
- All queries in
src/database/queries/. No raw SQL in screen components or hooks. - No array-level filtering on DB results. If you can write it as a
WHEREclause orJOIN, do that instead. Fetching 10,000 rows and filtering to 10 in JS is a performance bug. - Mutations via
syncedInsert/syncedUpdate/syncedDelete. These are insrc/database/syncedWrites.ts. They stampsyncUpdatedAtautomatically. Directdb.insert()/db.update()are only acceptable for tables that are explicitly not synced (e.g.locationPoints,tempNativePoints). - Soft deletes for synced tables. Call
syncedDelete()— never a hardDELETEon a synced table, as that loses the tombstone.
React components
- Function components only. No class components.
- Hooks for logic, components for rendering. Extract non-trivial business logic to a custom hook before a component grows long.
- NativeWind for styles. Use Tailwind classes. Avoid raw
StyleSheet.createunless you have a reason (e.g. complex animations where StyleSheet is required). - No inline
style={{}}props except for dynamic values (e.g.style={{ width: animated.value }}).
State
- React Query for async data. If data comes from a DB query, put it in a React Query hook. Don’t put fetched data in Zustand.
- Zustand for synchronous global state only. The two stores are
useActiveShiftanduseSettingsStore. Add to them sparingly — most features don’t need global state. useStatefor local component state. Modal open/close, text input values, etc.
Naming
- Files:
PascalCase.tsxfor components,camelCase.tsfor everything else. - Hooks:
useprefix, camelCase — e.g.useActiveShift,useGPSTracking. - Query keys: arrays of strings —
['shifts', 'recent'],['analytics', 'today'].
Comments
Default to no comments. Write self-documenting code (clear names, small functions). Add a comment only when the why is non-obvious:
// GPS jitter: >150 km/h implies a spike, not a real movement
if (impliedSpeedKmH > 150) continueDo not add comments that describe what the code does (the code already does that):
// BAD: increment the counter
count++
// BAD: loop through shifts
shifts.forEach(shift => { ... })Pull requests
- Open an issue first for non-trivial changes. Alignment on design before a large PR saves everyone time.
- One logical change per PR. Avoid bundling unrelated fixes or refactors.
- TypeScript must pass.
npx tsc --noEmitmust exit with no errors. - Lint must pass.
npm run lintmust exit with no errors. - Test what you build. If you add a query function, test it on a device or emulator with real data. Type checking verifies shape, not behavior.
PR description checklist
- What changed and why
- How to test it
- Screenshots / recordings for UI changes
- Any database migration included (describe what it does)
Database migrations
If your change requires a schema migration:
- Add a new migration entry in
src/database/client.ts. - Increment the version number.
- Write idempotent SQL:
ALTER TABLE shifts ADD COLUMN foo TEXT; -- Safe: SQLite allows re-running ADD COLUMN if the column already exists in some versions, -- or use: SELECT COUNT(*) FROM pragma_table_info('shifts') WHERE name='foo' - Test the migration on a fresh database AND on an existing database with real data.
- Note the migration in your PR description.
Adding a supported platform
Platforms are defined in src/registry/platforms/. Each country has its own file.
- Add an entry to the appropriate country file (or create a new one):
{ id: 'new_platform', label: 'New Platform', color: '#FF6B00', textColor: '#FFFFFF', country: 'US', logoEmoji: '🚚', defaultHourlyRate: '20', defaultMileageRate: '0.67', sortPriority: 10, } - The platform will automatically appear in the activation list for users in that country.
Feature flags
New features that aren’t ready for all users should be gated:
- Add the flag name to the type in
src/hooks/useFeatureEnabled.ts. - Set its default value in
src/registry/countries/(per country default). - Wrap the feature:
const isEnabled = useFeatureEnabled('my_feature') if (!isEnabled) return null - Add it to the developer feature override list in
app/settings/developer.tsx.
Commit style
Follow Conventional Commits:
feat: add expense receipt photo
fix: GPS jitter filter threshold too aggressive
refactor: extract mileage calculation to pure function
docs: update GPS engine architecture docTypes: feat, fix, refactor, docs, test, chore.
Reporting bugs
Open a GitHub issue with:
- App version (visible in About)
- Device model and OS version
- Steps to reproduce
- Expected vs. actual behavior
- Logs if available (Android:
adb logcat, iOS: Xcode console)
License
Comma is MIT licensed. By contributing, you agree your contributions are under the same license.