project-architecture
The master reference for how this project is structured — services, domains, database, and the rules for how services talk to each other. Consult this before creating any new service, folder, endpoint
SKILL.md.claude/skills/project-architecture/SKILL.md
GitHubProject Architecture — Master Reference
This project is a single monorepo containing independently deployed services sharing a shared database and running inside a single Railway project.
The Eight Services
The repository has two main regions: /store (for the storefront and store backend) and /studio (for backend tools and ops services):
| Service | Folder | Language | Deploys to |
|---|---|---|---|
| Medusa backend | store/backend |
Node.js (Medusa v2) | mysite.com/api |
| Storefront | store/storefront |
Node.js (Next.js) | mysite.com |
| AI Gateway | studio/ai-gateway |
Node.js (Express) | internal only |
| Dubbing | studio/dubbing |
Python (FastAPI) | studio.mysite.com/api/dub |
| Bot Bridge | studio/bot-bridge |
Node.js (Express) | internal only |
| Comment Bot | studio/comment-bot |
Node.js (Express) | studio.mysite.com/webhooks/comments |
| TTS Service | studio/tts-service |
Python (FastAPI) | internal only |
| Chatwoot | studio/chatwoot |
Docker compose | studio.mysite.com/chat |
The Two-Domain Split
mysite.com— customer-facing storefront and API only. Must stay fast and polished.studio.mysite.com— operations side: Chatwoot inbox (/chat), comment webhook (/webhooks/comments), dubbing upload/processing (/api/dub). Only you and paying dubbing clients see this.
Internal services (ai-gateway, bot-bridge, tts-service) are fully private and accessible only via Railway private networking.
The One Hard Rule: HTTP Only Between Services
Services NEVER import each other's code directly. They talk only over HTTP, using URLs from environment variables.
// ❌ NEVER
import { dubVideo } from '../dubbing/pipeline'
// ✅ ALWAYS
const res = await fetch(process.env.DUBBING_URL + '/api/dub', {
method: 'POST', body: JSON.stringify({ videoUrl })
})
Why: this is what makes it possible to move any single service to its own server/domain later by changing one env var, with zero code changes. Never hardcode localhost:PORT anywhere — always process.env.<SERVICE>_URL.
Standard service URL env vars:
STORE_URL=
STOREFRONT_URL=
DUBBING_URL=
AI_GATEWAY_URL=
TTS_SERVICE_URL=
CHATWOOT_URL=
Shared Database (Supabase)
One Supabase project. Each service owns its own tables but all share the same Postgres instance.
| Table area | Owned by |
|---|---|
products, orders, customers (Medusa's own schema) |
Store (own Postgres instance) |
conversations, bot_logs |
Bot Bridge |
dubbing_jobs, dubbing_chunks |
Dubbing |
ai_requests / ai_usage_logs (cost/usage log) |
AI Gateway |
comment_log |
Comment Bot |
api_keys |
Dubbing (Clients auth) |
service_health |
Shared (ops) |
Row Level Security (RLS) is mandatory on every table — see the security-engineering skill for the exact policy pattern. Schema migrations live in /architecture/migrations/ so any agent can see the full schema history.
Per-Service Convention (non-negotiable)
Every service folder must contain:
README.md— what it is, how to run it locally, which env vars it needs.env.example— every required var with a placeholder value (never real secrets).env— real secrets, gitignored, never committed
If a new service is created and is missing either file, that is a bug — fix it immediately.
AI Gateway Is the Only Door to AI Providers
No service calls Claude, Gemini, or MiniMax directly. All AI calls go through AI_GATEWAY_URL. See the ai-gateway skill for the contract. This is what lets you swap providers project-wide by editing one service.
When Adding a New Feature
- Does it belong in an existing service, or is it genuinely a new service? Default to existing unless it has its own deployment lifecycle.
- Does it need AI? → call the AI Gateway, don't call providers directly.
- Does it need new tables? → add a migration in
/architecture/migrations/, add RLS policies, update the table-ownership list above. - Does it cross a service boundary? → HTTP + env var, never a direct import.
- New service? → it needs its own README.md, .env.example, and an entry in the services table above and in the
deploymentskill.
Future-Splitting Checklist
If any service ever needs to become its own product on its own domain:
- Confirm it has zero direct imports from other services (grep for relative imports crossing service folders)
- Confirm its
.env.exampleis complete enough to deploy standalone - Deploy it to the new location, update its
_URLenv var in the other services - No other code changes should be required — if they are, that's a violation of the HTTP-only rule that should be fixed first