Contributing
Working rules
The rules every change to Startica follows, from where code goes to how AI calls, schema changes and UI components work.
These rules keep Startica consistent, cheap to run and safe for the people who use it. Reviewers check pull requests against them. Most of them come from decisions recorded in ARCHITECTURE.md at the repo root; if you think one is wrong, start a Discussion rather than working around it.
Product rules
These are promises to users. Changing any of them needs a Discussion first.
- The user always submits. Startica may fill in a reply or post (copy-and-open on every channel, and the posting window for X, Hacker News, Reddit, LinkedIn, Bluesky, Mastodon and Discourse), but the user always presses the site’s own button. Never click it, press Enter or submit a form programmatically, and don’t add “post for me”, scheduled or bulk posting, or code that comments, votes or sends messages on a platform.
- No AI in the browser. Fill-ins are fixed per-site scripts with known selectors, run in an isolated world. Never send page content to an AI model or let a model drive the posting window.
- No reading through the posting window. It only opens the one page being posted to, and only reads the response to the user’s own submit to find the new post’s link. Discovery and engagement stay on public feeds and APIs.
- Keep the posting window isolated. The site view gets no preload, no Node and no access to the server or launch token; it shows only the site’s hosts (for Mastodon and Discourse, the host in the request) and the sign-in pages in
SIGN_IN_HOSTS(Google, Apple, GitHub, GitLab, Discord, Microsoft); permission requests are refused. When you change a site’s selectors inapps/desktop/posting/sites.mjs, change its stand-in inapps/desktop/scripts/posting-standins.mjstoo. See Testing. - No scraping. Read only through official APIs or public feeds a platform offers (Reddit’s RSS feeds, the Hacker News Algolia API). No headless browsers against platforms (the posting window posts; it never reads), no unofficial endpoints, nothing that breaks a platform’s terms. Channels without a usable API stay manual (paste a link or text).
- Local first. No Startica server, no account, no telemetry. The only network calls are to services the user configured (their AI provider, connected metrics sources) and the public sources listed in Privacy and security. A new network destination needs a Discussion.
- Bring your own credentials. Don’t ship shared API keys or OAuth clients.
- No paid services beyond the ones the user chooses to connect.
Where code goes
| Code | Goes in |
|---|---|
| Business logic, AI prompts, jobs, channel adapters, metrics connectors | packages/core/src/<area>/ |
| Tables, migrations, queue, secrets, change events | packages/db/ |
| Pages, server actions, route handlers, UI components | apps/desktop-web/src/ |
| Window, tray, keychain, packaging | apps/desktop/ |
- Business logic lives in
packages/core, not in React components or route handlers. Server actions and jobs both call into core. - Pages and server actions get the database through
db()fromapps/desktop-web/src/lib/dal.ts. It callsconnection()and makes sure startup has run. Don’t callgetDb()directly from app code. - Server actions validate their input with Zod and return an
ActionResult. Themutate()helper insrc/lib/actions.tsrefreshes the page on success and turns errors into a message. - Scope every query to the workspace, and check that a product belongs to it with
productScope()before writing. - Inside the Next.js server, call the server itself with
node:http, notfetch. Next patches the globalfetch.
AI calls
- AI runs only in background jobs. Pages and server actions never call the AI. They enqueue a job and show its result when it lands. This keeps requests fast and every call in one logged path.
- Every call goes through
packages/core/src/ai/client.ts:runStructured()for data written to the database,runResearch()for prose with web search. No direct@anthropic-ai/sdkoropenaicalls anywhere else: the provider adapters (anthropic.ts,openai.ts) are called only by the client. The client handles model and provider routing, the monthly budget, prompt caching, refusal and truncation checks, and logging toai_runs. - Structured outputs are validated with Zod. Anything written to the database comes from
runStructured()with a Zod schema. Never parse free text into the database. - Model IDs live in config, not code. Each call names a task (
AiTask:ping,strategy,research,draft,vision,score,answer,image), and theai_modelstable maps tasks to a model and effort. Don’t hard-code a model in a job. - Keep the system prompt stable. It’s marked for prompt caching. Put volatile content (dates, the thread text) in the messages.
- Every prompt has a version string, like
SCORE_PROMPT_VERSION = "opportunity-score@1", passed aspromptVersionand logged with each run. Change the prompt, bump the version. If the prompt has an eval set, rerun it and put the result in the commit message. See Prompts and evals.
Database
- Schema changes: edit
packages/db/src/schema.ts, runyarn db:generate, and commit the new migration inpackages/db/migrations. Migrations are applied automatically on the next start. - Never edit a committed migration. Write a new one.
- Every table has a text UUID
id, aworkspace_idandcreated_at/updated_at. JSON goes in JSON text columns. - Keep
transaction()free of network and AI calls. The whole process waits while a transaction is open. - Text enums like
channelare checked by TypeScript only, not by SQLite. Adding a value doesn’t need a migration, but update every place that lists the values. - Rows the user wrote or edited must survive AI rebuilds. Follow the existing
sourcecolumn pattern ("ai"or user-written) when you add AI-generated rows.
UI
The app uses Tailwind CSS 4 and shadcn/ui in the base-nova style, which is built on Base UI, not Radix.
-
Compose with the
renderprop, notasChild:<DialogTrigger render={<Button size="sm" variant="outline" />}>Set up</DialogTrigger> -
Add components with the shadcn CLI, from
apps/desktop-web:cd apps/desktop-web yarn dlx shadcn@latest add <component> -
Icons come from
lucide-react. -
Prefer Tailwind classes in components. Edits to
globals.cssmay not hot-reload in a running dev server.
Page pattern (Cache Components)
A page’s default export renders its static parts (the PageHeader) and wraps an async content component in <Suspense fallback={<PageSkeleton />}>. All request-time reads happen inside that child, through db(). Code that reads the clock (Date.now()) belongs in plain loader functions, not in component bodies (a React lint rule). Add <LiveRefresh tables={[…]} /> with the tables the page shows, so it updates when a job writes to them.
Charts
Charts use Recharts and follow these rules:
- One y-axis per chart. Metrics on different scales get separate charts.
- Series colors are validated for both light and dark backgrounds and scoped on the chart element as CSS variables, for example
[--viz-series-1:#2a78d6] dark:[--viz-series-1:#3987e5]. - A crosshair tooltip shows exact values.
- Every chart has a “Show as table” fallback.
apps/desktop-web/src/app/(app)/analytics/metric-chart.tsx is the reference implementation.
Checks and hygiene
- Run
yarn typecheck && yarn lint && yarn test && yarn smoke:desktopbefore you open a pull request. For changes toapps/desktop, also runyarn workspace @startica/desktop smoke, and add--packagedafter a build. - Test processes use isolated ports and data. Ports 58397–58399, a temporary
STARTICA_DATA_DIR, and a separateSTARTICA_NEXT_DIST. Never start a test server on 58300. - Next.js renames its process to
next-server, so stop leftover test servers by port (lsof -tiTCP:58399), not by name. A stale Startica instance blocks new launches through the single-instance lock: check withpgrep -fl Startica. - Never commit API keys,
.envfiles,secrets/, service-account JSON,apps/desktop/buildorapps/desktop/dist, or.next*build folders. - Keep docs current. If you change a decision, update
ARCHITECTURE.mdat the repo root in the same pull request; when you build something listed inPLAN.md, describe it in ARCHITECTURE.md and remove it from PLAN.md. If you change behavior a user or contributor sees, update these docs.
Decide on your own
You don’t need to ask about library choices within the existing stack, file layout inside the packages, component structure, naming, test structure, or exact cron times. Use your judgment and explain it in the pull request.