Naming things
Names live forever. The cost of renaming a column is high; the cost of picking the wrong name today is permanent.
General principles
- Be specific:
dropped_off_atnotdate1 - Use the operator's vocabulary: bike-shop staff say "ticket," not "work order"
- Avoid prefix soup:
customer_idnotcust_idnotc_id - Plural for tables, singular for entities:
customerstable, "customer" in prose - No reserved words: avoid SQL reserved words even if technically allowed
Tables
snake_case, plural noun:
customers,service_tickets,inventory_skus,staff_sessions- Subordinate tables:
{parent}_{child}—customer_bikes,service_ticket_lines
Avoid:
tbl_customers(thetbl_is noise)customer(singular — should becustomers)customer_table(redundant)
Columns
snake_case. Suffix conventions:
*_idfor FK references (always integer)*_atfor TEXT timestamps in ISO 8601 (e.g.,created_at,paid_at)*_onfor DATE values (e.g.,dob,promised_on)*_centsfor money (always integer, never float)*_countfor countersis_*for boolean (stored as INTEGER 0/1)qty_*for quantities (e.g.,qty_on_hand,qty_committed)*_jsonfor serialized JSON TEXT columns (e.g.,before_json)*_hashfor hashed values
Avoid:
created_date,created_time— usecreated_at(ISO 8601)price— useprice_cents(avoids float rounding bugs)descriptionwhen something more specific exists —issue_description,internal_notes
Endpoints
/api/{plural-resource}/{id}/{sub-resource}. Always lowercase, hyphen-separated.
/api/customers— list/create/api/customers/123— detail/api/customers/123/bikes— sub-collection/api/customers/123/bikes/456— sub-detail/api/customers/123/merge— action (POST)/api/tickets/today— special view
Use POST for non-idempotent actions; PUT for idempotent updates; DELETE for deletion; GET for reads.
Functions
camelCase. Verb-noun pattern for handlers:
apiCustomersList,apiCustomersCreate,apiCustomersDetailapiTicketsCreate,apiTicketsStatusTransition
Helpers:
hashPin,verifyPin,taxForLine,withAuditpathParam,json,notFound
Don't:
getCustomers(too generic; conflicts with HTTP method)processStuff(what stuff?)customerHandler(one of many; doesn't say what it does)
Files
kebab-case.js. Match the contents:
src/lib/tax-bc.jsfor BC tax helperspublic/js/customers.jsfor the Customers screen enhancermigrations/004-service-tickets.sqlfor slice 4 schemadocs/architecture/c4-context.mdxfor the C4 context diagram
Constants
SCREAMING_SNAKE_CASE only for module-level true constants:
ADMIN_RESET_CODEPBKDF2_ITERATIONSSESSION_MAX_SECONDS
Configuration values that vary per shop go in shop_config, not in code constants.
Status enumerations
Lower-snake-case strings stored in TEXT columns:
- Ticket status:
dropped_off,quoted,in_progress,awaiting_parts,ready,picked_up,cancelled - Transaction status:
pending,paid,voided,refunded_partial,refunded_full
Don't use integers ("status 3 means ready"). Strings are self-documenting; the small storage cost is irrelevant.
Audit event actions
{entity}.{verb}, lower-snake:
customer.created,customer.updated,customer.mergedticket.status,ticket.line.added,ticket.cashed_outpermission.overridden
The verb tells what happened; the entity tells what changed.
When you can't decide
Pick the name that:
- The operator would use in conversation
- Future-you reading the field in 2 years would understand
- Greps well (avoid common-word names like
name,type,kindwithout prefix)
If still stuck: pick one, ship it, rename if it bothers you in 2 weeks. Renaming is cheap when the name is fresh.