Imported from iloveitaly/llm-ide-rules (
AGENTS.md). Install upstream withnpx skills add iloveitaly/llm-ide-rules. Copyright stays with the author.
Coding instructions for all programming languages:
- Never use emojis anywhere unless explicitly requested.
- If no language is specified, assume the latest version of python.
- If tokens or other secrets are needed, pull them from an environment variable
- Prefer early returns over nested if statements.
- Prefer
continuewithin a loop vs nested if statements. - Prefer smaller functions over larger functions. Break up logic into smaller chunks with well-named functions.
- Use named constants for magic numbers, service urls, etc. Do not duplicate magic strings or numbers in code.
- Prefer constants with separators:
10_000is preferred to10000(or10_00over1000in the case of a integer representing cents). - Prefix feature-flag style constants with
{DISABLED,ENABLED}_ - When I ask you to write code, prioritize simplicity and legibility over covering all edge cases, handling all errors, etc.
- When a particular need can be met with a mature, reasonably adopted and maintained package, I would prefer to use that package rather than engineering my own solution.
- Never add error handling to catch an error without being asked to do so. Fail hard and early with assertions and allow exceptions to propagate.
- When naming variables or functions, use names that describe the effect. For example, instead of
function handleClaimFreeTicket(a function which opens a dialog box) usefunction openClaimFreeTicketDialog. - Do not install missing system packages! Instead, ask me to install them for you.
- If terminal commands are failing because of missing variables or commands which are unrelated to your current task, stop your work and let me know.
- Don't worry about fixing lint errors or running lint scripts unless I specifically ask you to.
- When implementing workarounds for tooling limitations (like using
Anyfor unresolvable types) or handling non-obvious edge cases, always add a brief inline comment explaining the technical reasoning. - Reserve exact-width
#-box section separators for long files requiring organization, though they should not be necessary in the large majority of cases (separate files is generally better).
Use line breaks to organize code into logical groups. Instead of:
if not client_secret_id:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
session_id = client_secret_id.split("_secret")[0]
Prefer:
if not client_secret_id:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
session_id = client_secret_id.split("_secret")[0]
DO NOT FORGET: keep your responses short, dense, and without fluff. I am a senior, well-educated software engineer, and hate long explanations.
Add Comments for Expert Engineer with Limited Domain Knowledge
The engineer reading your code is a world-class software engineer, but is not familiar with the internals of every system. Include concise one-line comments explaining key hooks, API usage, blocks of logic, etc., to help the reader quickly understand the code you've written.
In other words, embed the business requirements as comments in the code when the code does not self-document.
Code Comments
- Describe behavior as it exists today—don’t frame comments around version history or “old vs new.”
- Only add comments if the code is not self-explanatory. Do not add obvious comments.
- Do not remove existing comments.
- Do not capitalize or add periods at the end of single-line comments.
Git Usage
- Do not automatically commit changes unless I explicitly ask you to.
- Never generate merge commits.
Important Workflow Rules
Pay careful attention to these instructions when running tests, generating database migrations, or otherwise figuring out how to operate this project:
- Run
justto understand the more important workflow commands.- Run
just --listto see all available pre-written workflow development commands.
- Run
- IMPORTANT: Never manually set environment variables that are required. You can set optional variables for debugging, but any missing required environment variables is an error that should be reported and you should stop your work immediately.
- Do not worry about cleaning up the environment. This is done automatically.
- Run python code with
uv run python - Use
pytestto run tests. If tests fail because of a configuration, environment, or system error: let me know and stop working.- Initially run
pytest --ignore=tests/integrationthen only runpytest tests/integration - When debugging integration tests look at
$PLAYWRIGHT_RESULT_DIRECTORY. There's a directory for each test failure. In that directory you fill find afailure.htmlcontaining the rendered DOM of the page on failure and a screenshot of the contents. Use these to debug why it failed.
- Initially run
- Do not attempt to create or run database migrations. Pause your work and let me know you need a migration run.
- If you receive errors about missing migrations, missing tables, database connectivity, etc, stop your work and let me know.
Alembic Migrations
- Migrations must stay compatible with offline SQL generation.
- Pass the session connection into helpers rather than opening a new connection.
- Prefer Alembic's enum integration over hand-written enum migrations when it works.
- Migration logic should not use model classes.
Default Content for New Non-Nullable Columns
To add a non-nullable column and set a specific value for all existing rows without a persistent server default:
# 1. Add the column as nullable (no default needed):
op.add_column(
"distribution",
sa.Column(
"default_campaign_ending_date", sa.DateTime(timezone=True), nullable=True
),
)
# 2. Update existing rows with your desired value (e.g., a specific datetime)
op.execute(
"UPDATE distribution SET default_campaign_ending_date = %s", [datetime.utcnow()]
)
# 3. Alter the column to non-nullable:
op.alter_column("distribution", "default_campaign_ending_date", nullable=False)
Record Backfill Operations
For migrations that include data mutation, and not only schema modifications, use this pattern to setup a session:
from alembic import op
from sqlmodel import Session
from activemodel.session_manager import global_session
from app import log
def run_migration_helper():
pass
def upgrade() -> None:
session = Session(bind=op.get_bind())
with global_session(session):
run_migration_helper()
flip_point_coordinates()
backfill_screening_host_data()
# flush before running any other operations, otherwise not all changes will persist to the transaction
session.flush()
However, if you don't need the business logic attached to the models, you can execute a query using op.execute:
op.execute(
TheModel.__table__.update().values({"a_field": "a_value"}) # type: ignore
)
Fastapi
- When throwing a
HTTPException, do not add adetail=and use a named status code (status.HTTP_400_BAD_REQUEST) - Do not return a
dict, instead create aclass RouteNameResponse- Locate these classes right above the
def route_name():function which uses them.
- Locate these classes right above the
- Use
Model.onewhen a record must exist in order for the business logic to succeed. - Do not try/except
Model.onewhen using a parameter from the request to pull a record. Let this exception bubble up. - Use
model_id: Annotated[TypeID, Path()]to represent a model ID as a URL path parameter - Use the typed route helpers in
app/generated/fastapi_typed_routes.pyfor all URL generation. - User-facing errors must not name internals. 3rd party API errors (Stripe, Clerk, etc) or internal implementation jargon should exist in error messages displayed to the browser. Think hard about user-facing error messages and make it clear what the user should do next.
Frontend Tests
- Do not add unit tests that duplicate Playwright coverage. Only add unit tests for edge cases which are not covered by Playwright.
Justfiles
- Docs: https://raw.githubusercontent.com/casey/just/master/README.md
- Never use
just_executable()to reference the executable forjust. IfjustDNE, then something is wrong adn you should stop your work and let me know. - You should not have to mutate
$PATH. If you cannot find an expected binary, stop your work and let me know. - Do not create aliases unless explicitly asked
- Separate scripts larger than 5 lines with newlines and comments for non-obvious logic
- Do not use inline shebang unless it differs from the default
Use the following script execution configuration:
# zsh is the default shell under macos, let's mirror it everywhere
set shell := ["zsh", "-ceuB", "-o", "pipefail", "-o", "extended_glob"]
# determines what shell to use for [script]
set script-interpreter := ["zsh", "-euB", "-o", "pipefail", "-o", "extended_glob"]
Python App
Here's how the python application is organized:
app/lib/is for code that is not specified to this application and with some effort could extracted into a external package.app/helpersis for larger reusable modules that if they weren't specific to this application, could be extracted into their own package.app/utilsare small helper functions that are specific to a particular page or area of the application.app/__init__.pyis the entrypoint for the application which is run when anything is executed (fastapi, celery, etc).- It primarily runs
configure_*commands for anyapp.configuration.*modules. These modules primary setup API clients, database connections, python language configuration, etc. - Also makes sure anything that mutates global state loads early.
- It primarily runs
- FastAPI server and routes are specified in
app/routes/ - SQLModels are specified in
app/models/ - Files within
app/commands/should have:- Are not designed for CLI execution, but instead are interactor-style internal commands.
- Should not be used on the queuing system
- A
performfunction that is the main entry point for the command. - Look at existing commands for examples of how to structure the command.
- Use
TypeIDfor any parameters that are IDs of models.
- Files within
app/jobs/should have:- Are designed for use on the queuing system.
- A
performfunction that is the main entry point for the job. - Look at existing jobs for examples of how to structure the job.
- Use
TypeID | strfor any parameters that are IDs of models.
- When referencing a command, use the full-qualified name, e.g.
app.commands.transcript_deletion.perform. - When queuing a job or
performing it in a test, use the full-qualified name, e.g.app.jobs.transcript_deletion.perform. app/cli/is for scripts or CLI tools that are specific to the application.- Webhooks should be fired in the model layer, not in a router or command.
- Lifecycle logic (status/state transitions, webhook enqueue, access grants) should live on the model. Commands, routes, etc should not re-implement
after_save-style logic. - Alias classes which are commonly used in the application. This makes it easier to grep for instances of that class without worrying about namespace clashes.
- Example:
from botocore.exceptions import ClientError as BotoCoreClientErrorinstead of a plainClientError. BaseModelfromactivemodelis commonly used inmodels/*.py, sopydantic'sBaseModelshould be aliased toPydanticBaseModel
- Example:
3rd Party APIs
- Always use an official client library if it exists.
- Be thoughtful about metadata fields. Only put data there for (a) reporting or (b) a joining key a downstream consumer actually reads. Do not duplicate keys or data in metadata fields without a clear and documented purpose.
- Log unexpected API responses or shapes to Sentry.
Python Test Code Organization
tests/**/utils.pyis for test-specific code that is not a fixture or a factory.app/factories/is the single source of truth for factories used by tests, local playground code, and dev seeding helpers. This is in theapp/folder so that it can be used by seeding scripts.tests/**/assertions.pyall customassert_*functions should go here.tests/**/conftest.pyis for test-specific fixtures. This is the only place you should put fixtures.tests/{commands,routes,jobs,models}/map to corresponding application categories underapp/.tests/integration/is for browser tests.- Certain complex 3rd party dependencies (like Stripe, Clerk, etc) may require complex helpers. If they do, create a
dedicated helper file in
tests/integration/**(i.e.stripe.py,clerk.py, etc) to contain helper methods.
- Certain complex 3rd party dependencies (like Stripe, Clerk, etc) may require complex helpers. If they do, create a
dedicated helper file in
Factories
globs: app/factories/**/.py
- Each model should get it's own file under
app/factories/model_name.py ActiveModelFactory(which is a polyfactory subclass) should be used.- Use
BaseFactory.__faker__to generate more specific fake data for important fields (used in routes, etc) - Prefer
slug = BaseFactory.__faker__.unique.slugtoslug = Use(lambda: BaseFactory.__faker__.unique.slug()) - Only override factory fields that differ from defaults.
Factory Example
class ScreeningFactory(ActiveModelFactory[Screening]):
funding_goal = lambda: BaseFactory.__faker__.random_int(min=0, max=2000_00)
ticket_price = DEFAULT_TICKET_PRICE
status = ScreeningStatus.active
# always None
funding_ending_at = None
# pick entry from a fixed list
zip_code = lambda: BaseFactory.__faker__.random_element(elements=REAL_ZIP_CODES)
host_name = BaseFactory.__faker__.name
host_description = lambda: BaseFactory.__faker__.paragraph(nb_sentences=2)
# this method runs before the model is persisted to the database
@classmethod
def post_build(cls, model):
# if the user does not pass in a important relationship during creation, you can generate a factory fallback
if not model.distribution_id:
model.distribution_id = DistributionFactory.save().id
return model.save()
# runs after the model is persisted to the database
@classmethod
def post_save(cls, model):
return model.save()
Database & ORM
When accessing database records:
- SQLModel (wrapping SQLAlchemy) is used
Model.one(primary_key)orModel.get(primary_key)should be used to retrieve a single record- Do not manage database sessions, these are managed by a custom tool
- Use
TheModel(...).save()to persist a record - Use
TheModel.where(...).order_by(...)to query records..where()returns a SQLAlchemy select object that you can further customize the query. - To iterate over the records, you'll need to end your query chain with
.all()which returns an interator:TheModel.where(...)...all()
- Use
- Instead of repulling a record
order = HostScreeningOrder.one(order.id)refresh it usingorder.refresh()
When writing database models:
- Don't use
Field(...)unless required (i.e. when specifying a JSON type for adictor pydantic model usingField(sa_type=JSONB)). For instance, use= Noneinstead of= Field(default=None). - Add enum classes close to where they are used, unless they are used across multiple classes (then put them at the top of the file)
- Use
ModelName.foreign_key()when generating a foreign key field - Store currency as an integer, e.g. $1 = 100.
before_save,after_save(self):,after_updated(self):are lifecycle methods (modelled after ActiveRecord) you can use.- Prefer to add constraints to the model over the frontend.
Example:
from pydantic import BaseModel as PydanticBaseModel
from activemodel import BaseModel
from typeid import TypeID
from activemodel.mixins import (
PydanticJSONMixin,
SoftDeletionMixin,
TimestampsMixin,
TypeIDPrimaryKey,
)
# pydantic models should be defined outside of the model class if they are used in other codes or models
class Video(PydanticBaseModel):
title: str
"name of the video"
class Distribution(BaseModel, TimestampsMixin, SoftDeletionMixin, table=True):
"""
Triple-quoted strings for multi-line class docstring
"""
# nest enum definition classes inside the model
# use `DistributionState` instead of `State` to avoid postgres enum name conflicts
class DistributionState(StrEnum):
pending = "pending"
active = "active"
archived = "archived"
id: TypeID[Literal["dst"]] = TypeIDPrimaryKey("dst")
date_field_with_comment: datetime | None = None
"use a string under the field to add a comment about the field"
# no need to add a comment about an obvious field; no need for line breaks if there are no field-level docstrings
title: str = Field(unique=True)
state: DistributionState = DistributionState.pending
optional_field: str | None = None
videos: list[Video] = Field(
sa_type=JSONB,
nullable=False,
default_factory=list,
# ensures there is a empty list when the record is created
sa_column_kwargs={"server_default": sa.text("'[]'")},
)
"videos available alongside the main film"
# here's how relationships are constructed
doctor_id: TypeID = Doctor.foreign_key()
doctor: Doctor = Relationship()
@computed_field
@property
def order_count(self) -> int:
return self.where(Order.distribution_id == self.id).count()
Python
When writing Python:
- Assume the latest python, version 3.13.
- Prefer Pathlib methods (including read and write methods, like
read_text) overos.path,open,write, etc. - docstrs and comments:
- If a docstring needs formatting, use markdown. Use Google Style.
- Prefer docstr to multi-line comments at the top of a function or file.
- If a docstr does not span multiple lines, do not use triple-quoted strings.
- If a comment or docstr is a single line, do not end it in a period.
- Add a newline after all docstrs.
- Do not create
__init__files unless specifically instructed - Use Pydantic models over dataclass or a typed dict.
- Use SQLAlchemy for generating any SQL queries.
- Use
clickfor command line argument parsing. - Use
log.info("the message", the_variable=the_variable)instead oflog.info("The message: %s", the_variable)orprintfor logging. This object can be found atfrom app import log.- Log messages should be lowercase with no leading or trailing whitespace.
- No variable interpolation in log messages.
- Do not coerce database IDs, dates, or Path objects to
str
- Do not fix import ordering or other linting issues.
- Never edit or create any files in
migrations/versions/ - Place all comments on dedicated lines immediately above the code statements they describe. Avoid inline comments appended to the end of code lines.
- Do not
try/catchrawExceptionsunless explicitly told to. Prefer to let exceptions raise and cause an explicit error. - Always make an explicit copy before mutating a dictionary that you did not create in the current narrow scope.
- Never use
from __future__ - Always use
re.compile(pattern, re.VERBOSE)and inline#comments to document the logic of each capture group or condition in any complex regular expression. - IMPORTANT never edit app/generated/ files. These are autogenerated.
Package Management
- Use
uv addto add python packages. No need forpip compile,pip install, etc.
Typing
- Assume the latest pyright version
- Prefer modern typing:
list[str]overList[str],dict[str, int]overDict[str, int], etc. - Prefer to keep typing errors in place than eliminate type specificity:
- Do not add ignore comments such as
# type: ignore - Never add an
Anytype. - Do not
cast(object, ...)
- Do not add ignore comments such as
Data Manipulation
- Prefer
funcyutilities to complex list comprehensions or repetitive python statements. import funcy as fandimport funcy_pipe as fp- Some utilities to look at:
f.compact
For example, instead of:
params: dict[str, str] = {}
if city:
params["city"] = city
if state_code:
params["stateCode"] = state_code
Use:
params = f.compact({"city": city, "stateCode": stateCode})
Date & DateTime
- Use the
wheneverlibrary for datetime + time instead of the stdlib date library.Instant.now().format_iso() - DateTime mutation should explicitly opt in to a specific timezone
SystemDateTime.now().add(days=-7)
React Router
- You are using the latest version of React Router (v7).
- Always include the suffix
Pagewhen naming the default export of a route. - The primary export in a routes file should specify
loaderDatalikeexport default function RouteNamePage({ loaderData }: Route.ComponentProps).loaderDatais the return value fromclientLoader. - Use
href("/products/:id", { id: "abc123" })to generate a url path for a route managed by the application.- Look at routes.ts to determine what routes and path parameters exist.
- Use
export async function clientLoader(loaderArgs: Route.ClientLoaderArgs)to define aclientLoaderon a route. - Do not define
Route.*types, these are autogenerated and can be imported fromimport type { Route } from "./+types/routeFileName" - If URL parameters or query string values need to be checked before rendering the page, do this in a
clientLoaderand not in auseEffect - Never worry about generating types using
pnpm - Use
<AllMeta />instead of MetaFunction or individual<meta />tags - Move derived config and business rules to the backend (computed fields, calculations, etc). Do not grow client-only sources of truth.
- Use outlets to store global config (such as Stripe keys, application settings, etc).
- Hide repeated mobile/desktop conditional styling behind a helper; individual components should not re-encode breakpoints
- Use the following pattern to reference query string values (i.e.
?theQueryStringParam=value)
const [searchParams, _setSearchParams] = useSearchParams()
// searchParams contains the value of all query string parameters
const queryStringValue = searchParams.get("theQueryStringParam")
Loading Mock Data
Don't load mock data in the component function with useEffect. Instead, load data in a clientLoader:
// in mock.ts
export async function getServerData(options: any) {
// ...
}
// in web/app/routes/**/*.ts
export async function clientLoader(loaderArgs: Route.ClientLoaderArgs) {
// no error reporting is needed, this will be handled by the `getServerData`
// mock loading functions should return result in a `data` key
const { data } = await getServerData({
/* ... */
});
// the return result here is available in `loaderData`
return data;
}
How to Use clientLoader
export async function clientLoader(loaderArgs: Route.ClientLoaderArgs) {- Load any server data required for page load here, not in the component function.
- Use
return redirect(href("/the/url"))to redirect users - Use getQueryParam to get query string variables
throw new Responseif you need to mimic a 400, 500, etc errorloaderArgsand all sub-objects are all fully typedloaderArgs.params.idto get URL parameters
Loading Backend Data
-
~/configuration/clientre-exports all types and functions fromclient/*. Import from~/configuration/clientinstead of anything you find in theclient/folder/package. -
For each API endpoint, there's a fully typed async function that can be used to call it. Never attempt to call an API endpoint directly.
- Do not generate types for API parameters or responses. Reference the autogenerated types that are re-exported in
~/configuration/client - For instance, the
getSignedUrlfunction in [web/client/sdk.gen.ts] has aSignedUrlResponsetype in [web/client/types.gen.ts] - This same type is used in the function signature, i.e.
type SignedUrlResponse = Awaited<ReturnType<typeof getSignedUrl>>["data"]
- Do not generate types for API parameters or responses. Reference the autogenerated types that are re-exported in
-
When using an import from
~/configuration/client:- use
body:for request params - always
const { data, error } = await theCall()
- use
clientLoader can only be used on initial page load within a route. If you need to load additional server data on component mount:
import { useQuery } from "@tanstack/react-query"
import {
// these options correspond to the server route
createCheckoutSessionOptions,
publicClient,
} from "~/configuration/client"
function TheComponent() {
const { data, error } = useQuery({
enabled: open,
...createCheckoutSessionOptions({
// or `client` if authenticated
client: publicClient,
body: { /* API parameters here */ },
}),
})
// remember to display errors by checking `error`
}
React
- You are using the latest version of React (v19)
- Do not write any backend code. Just frontend logic.
- If a complex skeleton is needed, create a component function
LoadingSkeletonin the same file. - Store components for each major page or workflow in
app/components/$WORKFLOW/$COMPONENT.tsx.- If a single page has more than two dedicated components, create a subfolder
app/components/$WORKFLOW/$PAGE/$COMPONENT.tsx
- If a single page has more than two dedicated components, create a subfolder
- Use lowercase dash separated words for file names.
- Use React 19, TypeScript, Tailwind CSS, and ShadCN components.
- Prefer function components, hooks over classes.
- Use ShadCN components in
web/app/components/uias your component library. If you need new components, ask for them.- Never edit the
web/components/ui/*.tsxfiles. - You can find a list of components here https://ui.shadcn.com/docs/components
- Never edit the
- Break up large components into smaller components, but keep them in the same file unless they can be generalized.
- Put any "magic" strings like API keys, hosts, etc into a "constants.ts" file.
- For React functional components with three or fewer props, always inline the prop types as an object literal directly in the function signature after the destructured parameters (e.g.,
function Component({ prop1, prop2 }: { prop1: string; prop2?: number }) { ... }). Include default values in destructuring and mark optional props with ? in the type object. Do not use separate interfaces or type aliases; keep types inline. For complex types, add inline comments if needed. - Put the interface definition right above the related function
- Internally, store all currency values as integers and convert them to floats when rendering visually
- When building forms use React Hook Form.
- Include a two line breaks between any
useHook()calls and anyuseState()definitions for a component. - When using a function prop inside a
useEffect, please use a pattern that avoids including the function in the dependency array, like theuseReftrick. - When writing React components, always hoist complex conditional expressions into descriptively named constants at the top of the component function for better readability and maintainability.
- When managing API response data, store the entire response object (or relevant subset) in a single
useStaterather than creating separate state variables for each field. Derive individual values from the response object when passing to child components using optional chaining (e.g., response?.field || defaultValue). - Refactor ternary to &&:
{condition ? <A/> : <B/>}→{condition && <A/>}{!condition && <B/>} - Use the following pattern to reference query string values (i.e.
?theQueryStringParam=value):
const [searchParams, _setSearchParams] = useSearchParams();
// searchParams contains the value of all query string parameters
const queryStringValue = searchParams.get("theQueryStringParam")
Mock Data
- For any backend communication, create mock responses. Use a async function to return mock data that I will swap out later for a async call to an API.
- When creating mock data, always specify it in a dedicated
web/app/mock.tsfile - Load mock data using a react router
clientLoader. Use the Skeleton component to present a loading state.
React Hook Form
Follow this structure when generating a form.
// add a mock function simulating server communication
async function descriptiveServerSendFunction(values: any) {
const mockData = getMockReturnData(/* ... */)
return new Promise(resolve => setTimeout(() => resolve(mockData), 500));
}
const formSchema = z.object({
field_name: z.string(),
// additional schema definition
})
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})
const {
formState: { isSubmitting, errors },
setError,
clearErrors,
} = form
async function onSubmit(values: z.infer<typeof formSchema>) {
clearErrors("root")
// ...
const { data, error } = await descriptiveSendFunction(values)
if (error) {
setError("root.serverError", { message: error.detail?.[0]?.msg })
return
}
// ...
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* form fields */}
<ServerErrorAlert error={errors.root?.serverError} />
<Button
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? "Submitting..." : "Submit"}
</Button>
</form>
</Form>
)
Styling
- Use
text-blue-linkfor styling any simple<a>tags
Shell
- Assume zsh for any shell scripts. The latest version of modern utilities like ripgrep (rg), fdfind (fd), bat, httpie (http), zq (zed), jq, procs, rsync are installed and you can request I install additional utilities.
Typescript
- Use
pnpmorpnpxand notnpmornpx.- Use
just js_shadcn,just pnpm, andjust js_lintinstead of executing these operations exactly. @just/javascript.just
- Use
- Node libraries are not available
- Use
lib/for generic code,utils/for project utilities,hooks/for React hooks, andhelpers/for page-specific helpers. - Prefer
function theName() {overconst theName = () => - Use
import { invariant } from @epic-web/invariantinstead of another invariant library - Use
requireEnv("VITE_THE_ENV_VAR")instead ofprocess.env.THE_ENV_VAR - Don't use
console.{log,error}. Usefrom ~/configuration/logging import logandlog.info("string", {structured: "log"})instead.
Here's how frontend code is organized in web/app/:
lib/not specific to the project. This code could be a separate package at some point.utils/project-specific code, but not specific to a particular page.helpers/page- or section-specific code that is not a component, hook, etc.hooks/react hooks.configuration/providers, library configuration, and other setup code.components/react components.ui/reusable ShadCN UI components (buttons, forms, etc.).shared/components shared across multiple pages.- create additional folders for route- or section-specific components.
Dates & Times
- Always use the ISO 8601 format when sending dates in an API request.
- Use
Temporalfor any date or time manipulation. You can assume it's available in the browser. - DateTime objects should always be converted to UTC before included in any API request. Never send a timestamp with the user's timezone.
- Unless otherwise specified, do not shift server-provided times based on the user's timezone.
Terraform
- All "bootstrap" secrets required to run the terraform code should be
exported in the Justfile. The Justfile is guarded so agents cannot run it, ensuring that required secrets are not available to agents ensures that terraform cannot be run by an agent. - Don't add a
localsfor a variable only used a single time - Do not add to terraform output unless asked to
- Consult the terraform MCP when using an external module. Module surfaces change often.
- When creating a worker, always set the
compatibility_dateto the current date. - Add
# sourced from ENVfor anyvariablesourced from the ENV. - Use
_convention, not[private]
File Structure
mise.tomlopentofu, environment variables, and other configuration required for terraform.onepassword_secrets.tfextracts secrets from 1Password that are required to run the terraform code. Secrets should be pulled from here, and not ENV.providers.tfdefines the providers to use.variables.tfdefines "global" variables, including those sourced from the ENV.deployment_state.tfstores the terraform state in a remote bucket.Justfilecontains the commands to run the terraform code. Never run it directly.
