>_devkit
naming-conventions
skills/naming-conventions/

references/projects.md

Naming projects and infrastructure

These names outlive the code. A repo name appears in clone URLs, registry paths, CI config, dashboards, and client conversations, so renaming one is a coordination exercise rather than a commit.

Repos and projects

<product>-<role>, kebab-case.

portsidemotors-api        portsidemotors-web        portsidemotors-admin
locatr-api                locatr-ui
devkit-site

Name the role, never the framework. A repo called portsidemotors-fast-api promises that the service will always be FastAPI. Frameworks get replaced; roles do not. -api, -web, -admin, -worker, -hooks, -ai describe what the thing is for, and survive a rewrite.

The same applies to -backend versus -api: prefer -api when it serves HTTP, since "backend" is a position rather than a responsibility.

House role vocabulary, so the same word always means the same thing:

SuffixMeans
-apiHTTP service, the system of record
-webPublic-facing site or app
-adminInternal operator UI
-workerBackground jobs, queue consumers
-hooksInbound webhook receivers
-aiModel-serving or inference service
-extBrowser extension
-uiShared component library, not an app

Design-system repos invert to category first, because the product is the aesthetic rather than a client: ui-apple, ui-gitlab. That grouping is deliberate and worth keeping: they sort together and read as a family. It is the one documented exception to <product>-<role>.

Templates and starters end in -template: devkit-tool-template.

Avoid a product name that is also a common word, and avoid dots (portsidemotors.code-workspace is a file, not a project).

Branches

dev, beta, prod. Environment names, matching the deploy targets exactly, so a pipeline rule and a compose file and a conversation all use the same word.

Do not mix in main, master, staging, or production. The value is that .env.<branch>, docker-compose.<branch>.yml, and the image tag :<branch> all derive mechanically from the branch name. That only works if the set is closed.

Working branches describe the change, kebab-case, optionally prefixed:

feat/refund-flow          fix/bid-race          chore/bump-deps

Never a bare name, a date, or initials. johns-branch and fix2 are unfindable a week later.

Commits

Conventional commits: type(scope): imperative summary.

feat(orders): add partial refunds
fix(promote): keep sourcing provenance out of the public vehicle
ci(rollback): keep .gitlab-ci.yml current during prod revert
chore(skill): remove code-reviewer

Types: feat, fix, chore, docs, test, ci, refactor, perf.

Scope is the domain area, not the file. feat(orders), not feat(admin_orders.py).

Summary is imperative and lowercase: "add refunds", not "Added refunds" or "Adds refunds". It completes the sentence "this commit will ...".

The body explains why, since the diff already shows what. A commit that changes behaviour without explaining the reason is a future archaeology session.

Services, containers, environments

Derived mechanically from the product and role, so nothing needs inventing:

# docker-compose.dev.yml
name: portsidemotors-dev                    # <product>-<env>

services:
  portsidemotors-api:                       # <product>-<role>
    container_name: portsidemotors-api-dev  # <product>-<role>-<env>
    image: registry.gitlab.com/<group>/portsidemotors-api:dev

Compose service names carry no environment, since the file already scopes them. Container names do, because they share one host namespace and a collision fails at runtime.

Image tags: the branch (:dev) for what a compose file pulls, plus the short SHA (:a1b2c3d) for what actually shipped. The branch tag moves; the SHA tag is the one you roll back to.

Env files: .env.<branch> for committed, non-secret, build-time values. .env.local for local secrets, gitignored. Never a committed file holding real secrets, whatever it is called.

Volumes name the data, not the service: portsidemotors-mongo-data.

HTTP endpoints

Plural kebab-case nouns, no verbs, version prefix.

GET    /v1/bank-accounts
GET    /v1/bank-accounts/{id}
POST   /v1/bank-accounts
DELETE /v1/bank-accounts/{id}

The HTTP method is the verb. /v1/getBankAccounts restates it wrongly and prevents caching semantics from being obvious.

kebab-case in paths, camelCase in the JSON body. Paths are read in browser bars and logs, where hyphens are conventional and underscores can be hidden by underlining.

Actions that are genuinely not CRUD get a sub-resource, still a noun where possible, and a verb only when no noun exists:

POST /v1/auctions/{id}/settlement        preferred
POST /v1/auctions/{id}/settle            acceptable when the noun is contrived

Nest at most one level. /v1/users/{id}/orders/{orderId}/lines/{lineId} is unusable; make lines a top-level resource filtered by order.

Audience prefixes stay in the path, not smuggled into the resource name: /v1/admin/orders, not /v1/admin-orders. That keeps one orders concept with two surfaces.

Environment variables

SCREAMING_SNAKE, grouped by area with a prefix.

MONGODB_URL              MONGODB_DB_NAME
AUTH_USERNAME            AUTH_PASSWORD           AUTH_SECRET
INSTALL_SECRET
REGISTRY_DIR             REGISTRY_POLL_SECONDS

The prefix is what makes a variable findable and what stops two subsystems colliding on TIMEOUT. Units belong here too: REGISTRY_POLL_SECONDS, not REGISTRY_POLL.

Framework-mandated prefixes take precedence and mean what the framework says: NEXT_PUBLIC_ marks build-time and client-visible, so never put a secret behind it.

CI jobs and stages

Stages are nouns, the phase: test, build, deploy, promote.

Jobs are verbs, snake_case, saying what runs: validate, lint, build_image, deploy, promote_to_beta, rollback_prod.

A job named after its tool (pytest, docker) ages badly the moment the tool changes. Name the intent.

Registry and package names

Scope to the organisation: @software-consultancy/<name>.

Monorepo release tags carry language and module so one repo can publish many artefacts independently:

ts/<name>/v1.2.0
py/<name>/v1.2.0

Single-artefact repos use plain v1.2.0. Always the v prefix, always full SemVer, never a bare number.