LAPLACE Fulfiller
Automated fulfillment framework that syncs order shipments from third-party warehouses without native Shopify integration to Shopify using the GraphQL Admin API.
Features
- Multi-Provider Support: Extensible architecture supporting multiple fulfillment providers
- Automated Monitoring: Checks provider orders every 5 minutes
- Smart Fulfillment: Only fulfills orders for specific provider warehouse locations
- Duplicate Prevention: Turso database tracks fulfilled orders across all providers
- Provider-Specific Logic: Each provider can have custom order extraction and tracking logic
- Type-Safe GraphQL: Uses Shopify's GraphQL Admin API (2025-07) with automatic type generation
- Built-in Providers:
- Rouzao - Chinese fulfillment provider
- HiCustom - Chinese POD fulfillment provider
Prerequisites
- Bun runtime
- API credentials for your fulfillment provider(s)
- Shopify store with API access
- Shopify locations matching your provider warehouses
Installation
bun installConfiguration
Create a .env file in the project root with the following variables:
# Turso Database Configuration (Required)
TURSO_DATABASE_URL=libsql://your-database-turso.io
TURSO_AUTH_TOKEN=your-turso-auth-token
# Shopify API Configuration (Required)
SHOPIFY_API_KEY=your_shopify_api_key_here
SHOPIFY_API_SECRET=your_shopify_api_secret_here
SHOPIFY_ACCESS_TOKEN=your_shopify_access_token_here
SHOPIFY_SHOP_DOMAIN=yourshop.myshopify.com
SHOPIFY_APP_URL=https://your-app-url.com
# Provider API Configurations
# Rouzao (automatically enabled when ROUZAO_TOKEN is set)
ROUZAO_TOKEN=your_rouzao_token_here
ROUZAO_LOCATION_IDS=location_id_1,location_id_2 # Optional: Comma-separated Shopify location IDs
# If not set, defaults to Rouzao's warehouse location IDs
# HiCustom (automatically enabled when API_KEY and API_SECRET are set)
HICUSTOM_API_KEY=your_hicustom_api_key
HICUSTOM_API_SECRET=your_hicustom_api_secret
HICUSTOM_LOCATION_IDS=location_id_1,location_id_2 # Optional: Comma-separated Shopify location IDs
# HICUSTOM_API_URL=https://api.hicustom.com # Optional: Override API base URL
# Add more providers as needed
# PROVIDER3_API_KEY=your_api_key
# PROVIDER3_API_SECRET=your_secretSetting up Turso Database
-
Install Turso CLI:
curl -sSfL https://get.tur.so/install.sh | bash -
Create a database:
turso auth signup # or turso auth login turso db create laplace-fulfiller -
Get database credentials:
# Get database URL turso db show laplace-fulfiller --url # Create an auth token turso db tokens create laplace-fulfiller -
Push schema to database:
bun run db:push
Note: If migrating from local SQLite, you'll need to export your data from the old database and import it into Turso. The schema remains compatible.
Getting Rouzao Token
- Log in to Rouzao (https://www.rouzao.com)
- Open browser developer tools (F12)
- Go to Network tab
- Perform any action that calls the API
- Look for the
Rouzao-Tokenheader in the request
Getting HiCustom Credentials
- Log in to HiCustom (https://www.hicustom.com)
- Navigate to API settings or developer section
- Create a new application or API client
- Copy the API Key and API Secret
- Note your Shopify location IDs that HiCustom will fulfill from
The HiCustom integration uses their OAuth API with automatic token refresh. See their API documentation:
Setting up Shopify API Access
- Create a private app in your Shopify admin
- Grant the following permissions:
- Read orders (
read_orders) - Write orders (
write_orders) - Read locations (
read_locations) - optional, but recommended - Read merchant-managed fulfillment orders (
read_merchant_managed_fulfillment_orders) - Write merchant-managed fulfillment orders (
write_merchant_managed_fulfillment_orders)
- Read orders (
- Copy the API credentials
Important: The fulfillment order permissions are required for the app to work properly. Without these permissions, you'll receive 403 Forbidden errors when attempting to process orders.
Shopify Location Setup
Each provider must have corresponding locations in Shopify. The provider will only fulfill orders from its registered locations.
You can run bun run diagnose to get the location IDs in your Shopify store.
For Rouzao:
- Set specific location IDs in
ROUZAO_LOCATION_IDSenvironment variable - Or create locations with names containing "Rouzao"
For HiCustom:
- Set specific location IDs in
HICUSTOM_LOCATION_IDSenvironment variable - Or create locations with names containing "HiCustom"
For other providers: Check the provider's locationIds or location name patterns
Running
Development
# Run with cron (continuous mode)
bun run start
# Run once and exit
bun run once
# Or with the flag directly
bun run src/index.ts --onceProduction
Deploy this application using the officially maintained container image from GitHub Container Registry. This is the only supported deployment method to ensure consistency, security, and compatibility.
Using the Official Image
# Pull the latest image
docker pull ghcr.io/laplace-live/fulfiller:latest
# Run the container
docker run -d \
--name laplace-fulfiller \
--restart unless-stopped \
--env-file .env \
ghcr.io/laplace-live/fulfiller:latestDocker Compose Example
Create a docker-compose.yml file for easier deployment:
services:
fulfiller:
image: ghcr.io/laplace-live/fulfiller:latest
restart: unless-stopped
env_file: .envThen run:
docker-compose up -dContainer Registry
The official image is publicly available at GitHub Container Registry:
# Pull the official image
docker pull ghcr.io/laplace-live/fulfiller:latest
# View available tags and versions
# Visit: https://github.com/laplace-live/fulfiller/pkgs/container/fulfillerThe image is automatically built and published with each release, ensuring you always have access to the latest stable version.
How It Works
- Provider Registration: On startup, all enabled providers are registered and initialized
- Order Monitoring: Every 5 minutes, the service fetches orders from all enabled providers
- Shipped Order Detection: Each provider filters its orders based on shipped status
- Order Details: For each shipped order, fetches detailed information including tracking
- Shopify Order Lookup: Each provider extracts the Shopify order number using its own logic
- Smart Fulfillment: Only fulfills items assigned to the provider's specific warehouse locations
- Duplicate Prevention: Records fulfilled orders in Turso database with provider context
Database
The service uses Turso (distributed SQLite) with Drizzle ORM to track fulfilled orders across all providers:
Schema:
provider- Provider identifier (e.g., 'rouzao')provider_order_id- Provider's order IDshopify_order_number- Shopify order numbershopify_order_id- Shopify order IDfulfilled_at- Fulfillment timestampcreated_at- Record creation timestamp
Features:
- Unique constraint on
(provider, provider_order_id)prevents duplicates - Indexed by provider and Shopify order number for fast lookups
- Old records (>365 days) are automatically cleaned up daily at midnight
- Type-safe queries with Drizzle ORM
- Global edge deployment with Turso for low-latency access
- Automatic backups and point-in-time recovery
- No file size limits unlike local SQLite
Logging
All activities are logged with ISO timestamps. Monitor the console output for:
- Order fetch results
- Shipped order processing
- Fulfillment success/failure
- Error messages
Troubleshooting
Common Issues
- "Rouzao location not found": Ensure you have a location named "Rouzao" or "柔造" in Shopify
- "Order already fulfilled": The order has already been processed or fulfilled in Shopify
- "Invalid third party order SN format": The order doesn't have a valid Shopify reference
- API errors: Check your API tokens and network connectivity
Debug Mode
To see more detailed logs, you can modify the console.log statements in the code or add additional logging.
Diagnostic Tool
Run the diagnostic script to check your Shopify API permissions and connections:
bun run diagnoseThis will test:
- Environment variable configuration
- Shopify API authentication
- Access scopes granted to your app
- Locations API access (lists all warehouse locations)
- Orders and Fulfillment Orders API access
- Rouzao location availability
Development
Tech Stack
- Runtime: Bun (fast all-in-one JavaScript runtime)
- Language: TypeScript with strict mode
- Database: Turso (distributed SQLite) with Drizzle ORM and @libsql/client
- API: Shopify GraphQL Admin API (2025-07)
- Type Generation: GraphQL Code Generator with Shopify preset
- Scheduling: Croner for cron jobs
- Code Quality: Prettier with import sorting
- Containerization: Production-ready Docker images available at GitHub Container Registry
GraphQL Type Generation
The project automatically generates TypeScript types from GraphQL queries:
# Generate types once
bun run graphql-codegen
# Watch mode for development (not configured)
# bun run graphql-codegen:watchGenerated types are stored in src/types/admin.generated.d.ts and should not be edited manually.
Database Management
Using Drizzle ORM for type-safe database operations:
# Generate migrations
bun run db:generate
# Apply migrations
bun run db:migrate
# Push schema changes directly (development)
bun run db:push
# Open Drizzle Studio (visual database browser)
bun run db:studioProject Structure
├── src/
│ ├── index.ts # Main application entry point
│ ├── lib/
│ │ ├── carriers.ts # Centralized carrier configuration
│ │ ├── db/
│ │ │ ├── client.ts # Database client and operations
│ │ │ └── schema.ts # Drizzle ORM schema definition
│ │ ├── providers/
│ │ │ ├── registry.ts # Provider registration and management
│ │ │ ├── rouzao.ts # Rouzao provider implementation
│ │ │ ├── hicustom.ts # HiCustom provider implementation
│ │ │ └── example.ts # Example provider template
│ │ ├── queries.graphql.ts # GraphQL queries and mutations
│ │ └── shopify.ts # Shopify GraphQL API integration
│ ├── scripts/
│ │ └── diagnose.ts # Diagnostic tool
│ ├── types/
│ │ ├── index.ts # Provider interfaces and types
│ │ ├── rouzao.ts # Rouzao-specific types
│ │ ├── hicustom.ts # HiCustom-specific types
│ │ └── admin.generated.d.ts # Auto-generated GraphQL types
│ └── utils/ # Utility functions
├── drizzle/ # Database migrations
├── references/ # Reference implementations (gitignored)
├── package.json
├── tsconfig.json # TypeScript config with path aliases
├── drizzle.config.ts # Drizzle ORM configuration
├── .graphqlrc.ts # GraphQL code generation config
└── .prettierrc.mjs # Code formatting configMulti-Provider Architecture
The application uses a provider-based architecture that makes it easy to add support for new fulfillment providers.
Provider Interface
Each provider must implement the Provider interface:
interface Provider {
id: string // Unique provider identifier
name: string // Human-readable name
locationIds: string[] // Shopify location IDs managed by this provider
// Check if provider has required configuration
isConfigured(): boolean
// Check if a location belongs to this provider
isProviderLocation(locationName: string, locationId: string): boolean
// Fetch shipped orders from the provider
fetchShippedOrders(): Promise<ProviderOrder[]>
// Fetch detailed order information
fetchOrderDetail(orderId: string): Promise<ProviderOrderDetail | null>
// Extract Shopify order number from provider's data
extractShopifyOrderNumber(orderDetail: ProviderOrderDetail): string | null
// Get tracking information
getTrackingInfo(orderDetail: ProviderOrderDetail): TrackingInfo
}Adding a New Provider
-
Copy the example template:
cp src/lib/providers/example.ts src/lib/providers/myprovider.ts -
Implement your provider logic:
- Update API endpoints and authentication
- Map your provider's data structure
- Configure carrier mappings and tracking URLs
- Add your Shopify location IDs
-
Register the provider in
src/lib/providers/registry.ts:import { myProvider } from './myprovider' // In the constructor this.register(myProvider) -
Add environment variables to
.env:MYPROVIDER_API_KEY=your-api-key MYPROVIDER_API_SECRET=your-secret -
Run the application - your provider will be automatically included!
Provider Features
- Automatic order tracking: Each provider's orders are tracked separately
- Custom business logic: Providers can implement their own order number extraction patterns
- Centralized carrier system: All providers share a unified carrier configuration
- Flexible carrier aliases: Different providers can use different codes for the same carrier
- Automatic tracking URLs: Generate tracking URLs based on carrier and tracking number
- Error isolation: Errors in one provider don't affect others
Centralized Carrier Configuration
The application uses a centralized carrier system (src/lib/carriers.ts) that:
- Defines carriers once: Each carrier has a name and tracking URL template
- Supports multiple aliases: Different providers can use different codes/names for the same carrier
- Provides unified lookup: All providers use the same functions to get carrier info
Example:
// Rouzao uses 'sf' for SF Express
// HiCustom uses '顺丰速运' for SF Express
// Both resolve to the same carrier with tracking URL
const trackingDetails = getTrackingDetails('sf', '123456')
// or
const trackingDetails = getTrackingDetails('顺丰速运', '123456')
// Both return:
// {
// carrierName: 'SF Express',
// trackingUrl: 'https://www.sf-express.com/.../123456'
// }To add support for a new carrier or alias:
- Edit
src/lib/carriers.ts - Find the carrier in the
CARRIERSarray - Add your provider's code/name to the
aliasesarray
Provider Configuration
Providers are automatically enabled or disabled based on their configuration:
Automatic Detection:
- If required environment variables are set → Provider is enabled
- If required environment variables are missing → Provider is disabled
For example:
# Rouzao will be enabled (has required token)
ROUZAO_TOKEN=abc123
ROUZAO_LOCATION_IDS=gid://shopify/Location/123,gid://shopify/Location/456 # Optional
# HiCustom will be enabled (has required credentials)
HICUSTOM_API_KEY=client123
HICUSTOM_API_SECRET=secret456
HICUSTOM_LOCATION_IDS=gid://shopify/Location/789,gid://shopify/Location/101 # Optional
# Example provider will be disabled (missing required key)
# EXAMPLE_API_KEY=How it Works:
Each provider implements an isConfigured() method that checks for required environment variables:
class MyProvider implements Provider {
isConfigured(): boolean {
return !!process.env['MYPROVIDER_API_KEY']
}
}Adding Features
- To modify the polling interval, change the cron expression in
src/index.ts - To add new GraphQL queries, edit
src/lib/queries.graphql.tsand runbun run graphql-codegen - To support additional carriers, update the mapping in your provider implementation
- All imports use the
@/path alias (e.g.,import { Provider } from '@/types')
License
AGPL-3.0