Skip to content

irfanf16/custimoo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Custimoo — Product Customization SaaS Platform

Production B2B SaaS — Multi-tenant white-label platform where merchant companies create customizable apparel products, their customers design and order them, and factories fulfill the production workflow. Live at custimoo.com  |  243+ merchant companies in production  |  Denmark

Laravel PHP Vue.js TypeScript MySQL Redis AWS S3 Stripe Shopify


Table of Contents


What is Custimoo?

Custimoo is a white-label product customization SaaS for the apparel industry. The platform has three distinct user types:

Actor Who What they do
Super Admin Custimoo staff Manages all companies, billing, Stripe config, system settings
Merchant Company Custimoo's B2B customers Creates products, manages customers, views orders, receives payments
End Customer The merchant's customers Browses products, customizes (colors/logos/text/roster), orders
Factory Production partner Receives orders, uploads production files, ships

Core flow:

  1. Merchant onboards → gets their own domain-scoped workspace (multi-tenant)
  2. Merchant creates Products with Styles, Designs, SKUs, pricing
  3. Customer logs in, browses products, customizes using the builder (Fabric.js + Three.js)
  4. Customer saves designs to Locker Rooms → organizes into Collections → places Orders
  5. Orders split by factory → go through a 12-stage production workflow
  6. Billing via Stripe (subscriptions for merchants, direct charges for shop orders)
  7. Integrates with Shopify / WooCommerce / BigCommerce for existing merchant stores

System Architecture

+-------------------------------------- CUSTIMOO PLATFORM --------------------------------------+
|                                                                                               |
|  Merchant Companies (Tenants)          Customers (End Users)        Factory Vendors           |
|  +--------------------------+          +---------------------+       +------------------+     |
|  | Admin SPA (Vue 3)        |          | Customizer (Vue 3)  |       | Vendor API        |     |
|  | Pinia + TanStack Query   |          | Fabric.js + Three.js|       | /api/vendor/*     |     |
|  +-----------+--------------+          +---------+-----------+       +--------+---------+     |
|              |                                   |                            |               |
|  +-----------v-----------------------------------------------------------------------------------+
|  |                         Laravel 12 REST API (PHP 8.2)                                         |
|  |                                                                                               |
|  |  /api/admin/*          /api/v2/*           /api/ecommerce/*     /api/vendor/*                |
|  |  (Passport OAuth)      (Custom JWT)        (Webhook Ingestion)  (Passport OAuth)             |
|  +--+---------------+----+-----------+--------+------------------+-+----------------------------+
|     |               |    |           |        |                  |                              |
|  +--v---+    +------v-+  |  +--------v----+  | +-------------+  |  +----------+               |
|  |Redis |    |AWS S3  |  |  |   Shopify   |  | |WooCommerce  |  |  |BigCommerce|              |
|  |Queue |    |Storage |  |  |   Webhooks  |  | |Webhooks     |  |  |Webhooks   |              |
|  |Horizon    +--------+  |  +-------------+  | +-------------+  |  +-----------+              |
|  +--+---+               |                   |                                                  |
|     |                   |  +-------------------------------------------------+                |
|  +--v-------------------v--v---+            | Third-party Services            |                |
|  |   MySQL (Shared DB)         |            | Stripe x2  SharePoint  Asana    |                |
|  |   ~125 tables, 421 migr.    |            | SendGrid   OpenAI      FedEx    |                |
|  |   company_id scoping        |            | DHL        Bugsnag     Qarma    |                |
|  +-----------------------------+            +-------------------------------------------------+ |
+-----------------------------------------------------------------------------------------------+

Tech Stack

Backend

Package Version Purpose
laravel/framework ^12.0 Core framework
laravel/passport ^13.0 OAuth2 — staff/factory token auth
laravel/horizon ^5.31 Redis queue monitoring dashboard
spatie/laravel-permission ^6.17 RBAC roles & permissions
stripe/stripe-php ^17.4 Stripe payments (subscriptions + Connect)
signifly/laravel-shopify ^1.2 Shopify API integration
aws/aws-sdk-php-laravel ^3.10 AWS S3 file storage
firebase/php-jwt ^6.11 Custom JWT auth for end customers
barryvdh/laravel-dompdf ^3.1 PDF generation (invoices, techpacks)
maatwebsite/excel ^3.1 Excel export/import
sendgrid/sendgrid ^8.1 Transactional email
spatie/icalendar-generator ^3.0 iCal event exports
intervention/image ^2.7 Server-side image processing
darkaonline/l5-swagger ^9.0 OpenAPI/Swagger documentation
bugsnag/bugsnag-laravel ^2.29 Error tracking
doctrine/dbal ^4.2 Schema introspection
ezyang/htmlpurifier ^4.18 HTML sanitization
lcobucci/jwt ^5.5 JWT library (secondary)

Frontend (separate repos)

  • Admin panel: development_admin_v2 — Vue 3, Pinia, TanStack Query v5, TypeScript, Tailwind
  • Product builder: development_custimoo_builder_v2 — Vue 3, Fabric.js, Three.js, Pinia, Tailwind v4

Infrastructure

  • Queue: Redis + Laravel Horizon (64 job classes)
  • Storage: AWS S3 (presigned URLs for direct browser upload)
  • Email: SendGrid
  • Errors: Bugsnag
  • PDF: External Node.js service (NODE_PDF_URL)

Multi-Tenant Architecture

Custimoo uses domain-based soft multi-tenancy — all tenants share one MySQL database, isolated by company_id. Tenant resolution happens per-request in the CheckCompany middleware.

Inbound Request
       |
       v
+------+-------------------------------+
| CheckCompany Middleware              |
|                                      |
| DomainCompany::resolve($request)     |
|                                      |
| Resolution priority:                 |
| 1. x-shopify-shop-domain (webhooks) |
| 2. producer + scope params (WP)     |
| 3. Origin header (browser SPA)      |
| 4. SubpageUrl + domain combo        |
| 5. ORIGIN env fallback (local)      |
+------+-------------------------------+
       |
       v
  companies table
  WHERE company_domains @> origin   <-- JSON array column
       |
       v
  $request->auth_company = $company
       |
       v
  All controllers scope queries by company_id

Key column: companies.company_domains is a JSON array of allowed origins (e.g. ["https://shop.client.dk", "https://app.client.com"]). Each company can have multiple domains.

Scoped tables (~25 tables carry company_id): customers, orders, products, customer_shops, factories, locker_rooms, collections, categories, email_templates, translations, brands, pricelists, settings, company_currency, company_languages, etc.


Authentication — Three Systems

1. Staff & Factory — Laravel Passport (OAuth2)

POST /api/admin/login  -->  Passport Bearer Token
                            stored in oauth_access_tokens
                            passed as: Authorization: Bearer {token}

Roles (via Spatie Laravel-Permission):

Role Scope
SuperAdmin Full system — all companies
admin Own company only
OrderAdministrator Order management
Factory Assigned factories only
Manager Specific customers
LimitedSuperAdmin Specific companies
AdminSalesRep Own company data
CSManager Super-admin scope
LogisticAssistant Specific factories
MerchantGroupAdmin Group-level admin

The CheckUserEntities middleware populates role_company_ids, role_customer_ids, role_factory_ids on the request for fine-grained data scoping.

2. End Customers — Custom JWT (firebase/php-jwt)

POST /api/v2/customer/login  -->  HS256 JWT  (SECRET_KEY env)
                                  passed as: CustomerToken: {token}
                                  stored in: customers.jwt_token
  • Customer model does not extend Laravel's Authenticatable
  • Token decoded in CheckCompany middleware, customer hydrated on $request->auth_customer
  • Anonymous pre-login state tracked by browser_key

3. Customer Shop Guests — Password Gate

customer_shops.password        (bcrypt hashed — for validation)
customer_shops.plain_password  (AES-256-CBC encrypted — shown to merchant)

AccessShopV2 middleware validates shop password on each request

Database Schema

421 migrations applied | ~125 tables | Shared MySQL DB

Domain A — Identity & Access

users                    company_id, name, email, password, commission, asana_user_id
customers                company_id, first_name, last_name, email, browser_key, jwt_token, role_id
roles / permissions      Spatie RBAC (guard: api)
model_has_roles          role_id, model_type, model_id  (polymorphic)
oauth_access_tokens      Passport tokens
ip_white_lists           company_id, IP allowlist
user_entities            user_id, sourceable_id, sourceable_type  (polymorphic scope)
user_tasks               user_id, assigned_to, task_details(JSON), relation_type, relation_id

Domain B — Companies, Billing & Subscriptions

companies                company_name, company_domains(JSON), admin_domains(JSON),
                         platform(enum: self/wordpress/cdnExceptLogin/shopify),
                         stripe_account_id, stripe_charges_enabled,
                         stripe_onboarding_completed_at, region, pricelist_id

billing_data             company_id, stripe_api_integration(enum: DEFAULT/CANADIAN),
                         stripe_id, invoice_prefix, tax_identifier,
                         payment_method(enum), stripe_tax_id

company_subscription_details  company_id, subscription_status, stripe plan fields

factories                company_id, name, email, asana_project_id, asana_workspace_id
pricelists               name, currency_id
rules                    pricelist_id, pricing rule definitions
brands                   company_id, name, logo
buying_groups_sales_reps type(enum: Buying Group / External Sales Rep)

Domain C — Product Catalog

products                 company_id, factory_id, parent_id, product_type(enum),
                         is_published, is_private, allowed_logos_count,
                         allow_name_number, merge_styles, svg_group_color_container(JSON)

product_styles           product_id, name, image, three_d_properties(JSON)
product_designs          product_id, product_style_id, svg_parts(JSON), custom_text(JSON)
                         [83K rows -- design state snapshots]

product_sizes            product_id, size definitions
product_colors           product_id, color definitions
product_logos_setting    product_id, product_style_id, logo_technologies(JSON)

sku_ids                  name, parent_id (self-ref), production_days,
                         asana_task_template_id, customized_sku_info(JSON)
                         [837 SKUs]

sku_currency             sku_id, currency_id, price, net_price
addon_groups             grouping for addons
add_ons                  sku_id, name, details
ecommerce_products       product_id, company_id, ecommerce_product_id, size_variants(JSON)

Domain D — Customer Shops (B2B2C)

customer_shops           company_id, customer_id, name, status(enum: draft/live/expired),
                         is_price_visible, template(enum), publish_at, unpublish_at,
                         password (hashed), plain_password (AES encrypted)

shop_slugs               customer_shop_id, name (URL slug, unique)
shop_products            customer_shop_id, product_id, title, images(JSON), sizes(JSON)
shop_settings            shop_id, key, value(JSON)

Domain E — Locker Rooms, Collections & Designs

product_locker_room      customer_id, company_id, product_id, product_style_id,
                         colors(JSON), custom_logos(JSON), defaultcolors(JSON),
                         product_roster_detail(JSON), text(JSON), images(JSON)
                         [55K saved customizations]

locker_rooms             customer_id, company_id, name
locker_room_assets       locker_room_id, asset_url, type  [88K assets]

collections              customer_id, company_id, locker_room_id, name,
                         pdf_link, ecommerce_collection_id  [34K collections]

design_files             name, url, type, svg_parts(JSON), customer_id
                         [275K design files on S3]

customer_logos           customer_id, url, logo_colors(JSON), is_ai_generated
                         [192K logos]

ai_logo_metas            customer_logo_id, prompt, mood(JSON), generation metadata
fixed_logos              company_id, product_style_id, url, is_customizable
data_containers          company_id, name, type
container_files          data_container_id, file URLs, SVG paths

Domain F — Cart & Orders

carts                    customer_id, company_id, customer_shop_id
cart_items               cart_id, factory_id, factory_products(JSON)

orders                   order_no, customer_id, company_id, billing_company_id,
                         ecommerce_order_id, shipping_address(JSON),
                         techpack_data(JSON), price_info(JSON),
                         stripe_invoice_info(JSON), payment_status,
                         sharepoint_path, customer_shop_id, is_shop_order

order_items              order_id, factory_id, status(enum -- 12 stages),
                         factory_products(JSON),  <-- full snapshot at order time
                         price_info(JSON), techpack_data(JSON),
                         stripe_invoice_id, asana_task_gid,
                         tracking_link, tracking_no, shipping_company

Domain G — Invoice Ledger (Stripe)

order_item_invoices          order_item_id, stripe_invoice_id(unique),
                             stripe_invoice_number, status, amount_due, amount_paid,
                             currency, hosted_invoice_url, allocations_snapshot(JSON),
                             idempotency_key, requires_manual_review

order_item_invoice_line_allocations  order_item_invoice_id, order_item_id,
                                     line_key, quantity

stripe_payment_attempts      order_id, stripe_checkout_session_id(unique),
                             connected_account_id, currency, amount_minor,
                             status(enum: pending/succeeded/failed/canceled/expired)

stripe_webhook_events        event_id(PK=Stripe event id), event_type,
                             connected_account_id, payload(JSON),
                             processing_status  -- idempotency deduplication

Domain H — Activity & Audit

order_item_activities       order_item_id, status(enum), action_log(JSON)  [65K rows]
order_item_activity_comments  activity_id, comment, files(JSON)  [33K rows]
order_activity_logs         order_id, action_type(enum), activity_meta_data(JSON)
complaint_attachments       complaint_id, file_url

Domain I — System

settings        sourceable_id, sourceable_type (polymorphic), key_name, value(JSON)
                -- per-entity key-value store (4.4K rows)
translations    company_id, locale, key, value  -- 11 languages supported
jobs / failed_jobs  Laravel queue tables
notifications   notifiable_type, notifiable_id, data(JSON)  [56K rows]
email_templates company_id, key_name, subject, body

API Structure

Prefix Auth Description
POST /api/admin/login Public Staff login → Passport token
/api/admin/* Passport Bearer Admin portal — companies, orders, products, customers, factories
/api/v2/* Custom JWT (CustomerToken) Customer-facing — customizer, locker rooms, cart, orders, shops
/api/ecommerce/* API secret header Shopify, WooCommerce, BigCommerce webhooks
/api/vendor/* Passport Bearer Factory vendor — order detail, techpack, QA report

Key admin endpoints:

GET/POST/PUT  /api/admin/orders              Order management
GET/POST/PUT  /api/admin/products            Product CRUD + styles/designs/SKUs
GET/POST/PUT  /api/admin/customers           Customer management
GET/POST/PUT  /api/admin/factories           Factory management
POST          /api/admin/stripe-connect/*    Stripe Express onboarding
GET/POST      /api/admin/roles              RBAC management

Key customer V2 endpoints:

POST  /api/v2/customer/login               JWT auth
POST  /api/v2/ai/generate-logo             OpenAI logo generation
POST  /api/v2/ai/redesign-logo             AI logo redesign
GET   /api/v2/locker-rooms                 Saved designs
POST  /api/v2/collections                  Create collections
POST  /api/v2/cart/add                     Add to cart
POST  /api/v2/orders                       Place order
GET   /api/v2/customer-shops               Browse B2B shops
POST  /api/v2/stripe-connect/checkout/create-session  Shop payment

Order Lifecycle

                 Customer places order
                         |
                         v
              +----------+----------+
              |   Order created      |
              |   Splits by factory  |
              +----------+----------+
                         |
          +--------------v--------------+
          |  submitted_for_factory_review |
          +--------------+--------------+
                         |
            +------------+------------+
            |                         |
    +-------v------+         +--------v-------+
    |factory_approved|       |factory_rejected |---> end
    +-------+------+         +-----------------+
            |
            v
    +-------+-------------------+
    |submitted_for_customer_review|
    +-------+-------------------+
            |
   +--------+---------+
   |                  |
   v                  v
customer_approved  customer_rejected
   |                  |
   |                  +---> back to factory
   v
production_files_uploaded
   |
   v
quality_control
   |
   v
in_production
   |
   v
awaiting_consolidation
   |
   v
shipped  (tracking_link + tracking_no added)
   |
   v
completed

Each status change creates a row in order_item_activities (65K rows) with full action_log JSON. Comments with file attachments tracked in order_item_activity_comments (33K rows).

Asana task created per order item (asana_task_gid stored). SharePoint path stored per order for techpack/file archival.


Stripe Dual-Account System

Custimoo operates two legal Stripe entities for EU and Canadian billing:

Company
  |
  v
billing_data.stripe_api_integration
  |
  +-- DEFAULT  -->  Custimoo ApS (Denmark)
  |                 env: DEFAULT_STRIPE_PRIVATE_KEY
  |                 env: DEFAULT_STRIPE_CONNECT_WEBHOOK_SECRET
  |
  +-- CANADIAN -->  Custimoo Canada Ltd
                    env: CANADIAN_STRIPE_PRIVATE_KEY
                    env: CANADIAN_STRIPE_CONNECT_WEBHOOK_SECRET

StripeConnectKeyResolver static class handles routing — all Stripe API calls call StripeConnectKeyResolver::client($company) which returns the correct StripeClient instance.

Webhook deduplication: Both accounts post to the same endpoint. The handler tries all configured webhook secrets until one verifies, then checks stripe_webhook_events table by event_id (PK) for idempotency before processing.

Stripe Connect flow (merchant onboarding):

1. Admin: POST /api/admin/stripe-connect/connect
          --> StripeConnectOnboardingService::ensureConnectedAccount()
          --> Creates Stripe Express account on correct platform
          --> Stores stripe_account_id on companies table

2. Admin: POST /api/admin/stripe-connect/account-link
          --> Generates hosted Stripe onboarding URL
          --> Merchant completes KYC on Stripe

3. Webhook: account.updated
          --> stripe_charges_enabled, stripe_payouts_enabled updated on company

4. Customer: POST /api/v2/stripe-connect/checkout/create-session
          --> Creates Checkout Session as direct charge on merchant's account
          --> Records attempt in stripe_payment_attempts

5. Webhook: checkout.session.completed
          --> Order marked paid

eCommerce Integrations

Platform Routing

POST /api/ecommerce/shopify-order-webhook
     |
     v
  company.settings['customizer_version']
     |
     +-- v1  -->  OrderController (legacy)
     +-- v2  -->  ShopifyOrderService (extracts _custimoo_cart_id from line item properties)

Shopify

  • SDK: signifly/laravel-shopify
  • Settings stored: settings table — key shopify_setting (store domain, access token, API version 2024-07)
  • Webhooks registered: order create, product update
  • Bidirectional product sync via sync_id column on products
  • Customer sync: CreateShopifyUserJob
  • Collection export: export-collection-to-shopify endpoint
  • Token refresh: daily ShopifyRefreshTokenCommand

WooCommerce

  • Settings: woocommerce_setting (api_username, api_password, store_url, plugin_api_secret)
  • Order import via _custimoo_cart_id WooCommerce order meta
  • Product sync: POST /api/ecommerce/woocommerce-update-product-webhook
  • Customer sync: CreateWooCommerceCustomerJob

BigCommerce

  • Settings: bigcommerce_setting (access_token, store_hash, client_id, channel_id)
  • Product sync: POST /api/ecommerce/bigcommerce-update-product-webhook
  • Order import: POST /api/ecommerce/bigcommerce-order-webhook
  • Addon modifiers: ecommerce_products.ecommerce_modifier_id

Background Jobs & Scheduling

64 queue job classes processed via Redis + Laravel Horizon.

Schedule Job / Command
Daily 00:00 FindMissingSharepointPathOrderIdsJob
Daily 03:00 CreateMissingSharepointAssetsOrderIdsJob
Daily 00:00 UpdateHolidays — sync public holidays from Calendarific API
Daily HandleShopStatusJob — activate/expire shops by publish_at/unpublish_at
Daily app:delete-expired-entity-tokens
Daily ShopifyRefreshTokenCommand — refresh Shopify API tokens
Daily 08:00 GenerateShipmentCsvJob
Daily ConsolidateOrdersDailyJob — merge shop order items
Daily 02:00 stripe-connect:cancel-unpaid-orders
Twice daily UpdateNewTrackingJob — FedEx/DHL tracking updates

Key jobs:

  • CreateShopifyUserJob / CreateWooCommerceCustomerJob / CreateBigCommerceCustomerJob
  • PostOrderDataToQarma — QA gateway integration
  • VectorizeOrderLogos — AI logo vectorization
  • CopyWoffFilesToS3DevelopBucket — font asset sync
  • GenerateTechpackPdfJob — order production PDF
  • SendOrderStatusEmailJob — SendGrid transactional emails

Third-Party Integrations

Service Purpose Auth
Stripe (x2) Subscriptions + Connect direct charges Secret key (per entity)
AWS S3 All file/asset storage, presigned upload URLs IAM key/secret
Shopify Order + product + customer sync Per-store access token
WooCommerce Order + product sync API key/secret
BigCommerce Order + product sync Access token + store hash
SharePoint (Graph API) Techpack + order file archival TENANT_ID, CLIENT_ID, CLIENT_SECRET
Asana Order workflow task creation ASANA_TOKEN
OpenAI AI logo generation (generate/redesign/refine) OPENAI_API_KEY
SendGrid Transactional email SENDGRID_API_KEY
FedEx Shipment tracking FEDEX_CLIENT_ID/SECRET
DHL Shipment tracking DHL_API_KEY
Calendarific Public holiday calendar sync CALENDER_API_KEY
Bugsnag Error monitoring BUGSNAG_API_KEY
Graip Factory QA data exchange Webhook-based
Qarma QA gateway QARMA_API_URL/USERNAME/PASSWORD
Node.js PDF service PDF/techpack generation NODE_PDF_URL, NODE_TECHPACK_URL

Environment Variables

# App
APP_KEY=
APP_URL=
APP_ENV=production

# Database
DB_CONNECTION=mysql
DB_HOST=
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=

# Redis (Queue + Cache)
REDIS_HOST=
REDIS_PASSWORD=
QUEUE_CONNECTION=redis

# AWS S3
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=
AWS_BUCKET=
S3_PRESIGNED_UPLOAD_ACL=public-read

# Stripe — DEFAULT account (EU/Denmark)
DEFAULT_STRIPE_PRIVATE_KEY=
DEFAULT_STRIPE_CONNECT_WEBHOOK_SECRET=

# Stripe — CANADIAN account
CANADIAN_STRIPE_PRIVATE_KEY=
CANADIAN_STRIPE_CONNECT_WEBHOOK_SECRET=

# Stripe Connect feature flag
STRIPE_CONNECT_PAYMENTS_ENABLED=false

# Shopify
SHOPIFY_API_VERSION=2024-07

# SharePoint (Microsoft Graph)
TENANT_ID=
CLIENT_ID=
CLIENT_SECRET=

# Asana
ASANA_TOKEN=
ASANA_WORKSPACE_ID=

# OpenAI
OPENAI_API_KEY=

# SendGrid
SENDGRID_API_KEY=

# FedEx
FEDEX_CLIENT_ID=
FEDEX_CLIENT_SECRET=

# DHL
DHL_API_KEY=

# Calendarific (holidays)
CALENDER_API_KEY=

# External PDF service
NODE_PDF_URL=http://localhost:3000
NODE_TECHPACK_URL=http://localhost:3001

# Bugsnag
BUGSNAG_API_KEY=
LOG_EXCEPTION_TO_BUGSNAG=true

# Qarma QA
QARMA_API_URL=
QARMA_USERNAME=
QARMA_PASSWORD=

# JWT (customer auth)
SECRET_KEY=

# Passport
PASSPORT_PRIVATE_KEY=
PASSPORT_PUBLIC_KEY=

Getting Started

composer install
cp .env.example .env
php artisan key:generate
php artisan passport:keys
php artisan migrate
php artisan passport:client --personal
php artisan horizon          # queue dashboard
php artisan serve

License

Proprietary — all rights reserved.

About

Multi-tenant product customization SaaS — Laravel 12, Vue 3, Stripe (x2), Shopify, WooCommerce, BigCommerce, 95+ models

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages