Deployable Shopify app that ingests Shopify store data to MySQL (Railway) for one or more tenants (stores).
See the ER diagram and DB design below for a visual overview of the schema and relationships. The image is located at public/db_design.png.
This repository contains the Shopify app that you install into your stores (tenants). It authenticates with Shopify, receives webhooks for ongoing updates, and supports manual historical ingestion. Data is stored in a shared MySQL database on Railway with a multi‑tenant schema.
- Framework: Remix (embedded Shopify app)
- Data: Prisma ORM + MySQL (Railway)
- Shopify: App OAuth, Admin GraphQL, Webhooks
- Remix 2.x + Vite dev server
- @shopify/shopify-app-remix for OAuth/session/webhooks
- Prisma ORM targeting MySQL (Railway)
Prerequisites: Node 18+, Shopify Partner account, a dev store, Railway MySQL.
Install dependencies
npm installConfigure environment in a .env file
# .env
DATABASE_URL=mysql://user:password@host:port/dbPrepare the database
npx prisma generate
npx prisma db pushRun the app (dev)
shopify app dev --store your-store.myshopify.comTip: If the preview opens too early and fails, press
pafter the server URL appears to re‑open it.
Ingest data
- Trigger manual ingestion for Products, Customers, and Orders from within the app.
- Live updates arrive via webhooks once installed.
Notes
- Multi‑tenant: When you install into another store, a new Tenant row is created automatically by domain.
The database schema is fully normalized and designed for multi-tenant analytics:
-
Relationships:
- Each
Tenant(Shopify store) owns its ownCustomers,Products,Orders, andEvents. CustomerandOrderare linked, so you can track purchases per customer.Eventlogs all webhook-driven activity (cart, checkout, etc.), linked to both tenant and customer.- Foreign keys enforce referential integrity between entities.
- Each
-
Normalization:
- All tables are normalized to at least 3NF (Third Normal Form): no redundant data, atomic fields, and all relationships managed via foreign keys.
- Composite keys (e.g.,
[tenantId, shopifyId]) ensure uniqueness per store, supporting multi-tenant isolation. - No transitive dependencies—customer details are stored only in
Customer, not duplicated inOrderorEvent.
-
Benefits for analytics:
- Clean joins and queries: You can easily aggregate metrics (e.g., top customers, product sales, abandoned carts) without worrying about data duplication or inconsistency.
- Data integrity: Updates to a customer or product propagate everywhere, so insights are always accurate.
- Scalability: Each store’s data is isolated, so you can onboard new tenants without risk of cross-store contamination.
- Flexibility: Normalized design makes it easy to add new features (e.g., new event types, advanced segmentation) without schema rewrites.
This design enables powerful, real-time analytics and insights for any number of Shopify stores, with guaranteed data quality and performance.
- Multi‑tenant data model keyed by store domain
- Manual historical ingestion of Products, Customers, Orders
- Live updates via Shopify webhooks
- Historical: Trigger manual ingestion in the app to pull from Shopify Admin GraphQL and persist via Prisma.
- Automatic: Webhooks for products/customers/orders create or update rows in the database on every change.
- products/create, products/update
- customers/create, customers/update
- orders/create, orders/update
- Requires proper Admin API scopes (read_products, read_customers, read_orders, etc.).
- Manual ingestion currently performs full scans. For very large stores, consider adding cursored batching and/or background workers.
- Webhook processing runs in‑process. For bursty traffic, consider using a queue (Redis/RabbitMQ) and a worker.
- Always filter queries by tenantId to maintain data isolation across stores.
Made by Dave Meshak | Portfolio
GNU GENERAL PUBLIC LICENSE. See LICENSE file for more details.