External Stores Module
Overview
The external_stores module provides a unified interface for integrating with external e-commerce platforms (Etsy, Shopify, Amazon, etc.), following the same pattern as the social_provider module.
Architecture
Module Structure
src/modules/external_stores/
├── index.ts # Module definition & exports
├── service.ts # ExternalStoresService (provider registry)
├── types.ts # Common interfaces
├── store-provider-registry.ts # Provider registry implementation
├── etsy-service.ts # Etsy API v3 implementation
└── (future) shopify-service.ts # Shopify implementation
Key Components
- ExternalStoresService - Main service that manages store providers
- StoreProviderRegistry - Registry for provider instances
- Store Providers - Individual platform implementations (EtsyService, etc.)
Store Provider Interface
All store providers must implement the StoreProvider interface:
interface StoreProvider {
name: string
// OAuth methods
getAuthorizationUrl(redirectUri: string, scope: string, state: string): Promise<string>
exchangeCodeForToken(code: string, redirectUri: string, state?: string): Promise<TokenData>
refreshAccessToken(refreshToken: string): Promise<TokenData>
// Store info
getShopInfo(accessToken: string): Promise<ShopInfo>
}
Common Types
interface TokenData {
access_token: string
refresh_token?: string
token_type: string
expires_in?: number
scope?: string
retrieved_at?: number
}
interface ShopInfo {
shop_id: string
shop_name: string
shop_url?: string
currency?: string
country?: string
[key: string]: any
}
interface ListingData {
title: string
description: string
price: number
quantity: number
images?: string[]
tags?: string[]
category_id?: string
[key: string]: any
}
interface ListingResponse {
listing_id: string
listing_url: string
status: string
[key: string]: any
}
Etsy Service Implementation
Features
- OAuth 2.0 - Full authorization flow with token refresh
- Shop Management - Get shop information
- Listing Management - Create, update listings
- Image Upload - Upload product images to listings
Environment Variables
# Required
ETSY_CLIENT_ID=your_etsy_keystring
ETSY_CLIENT_SECRET=your_etsy_secret
# OAuth
ETSY_REDIRECT_URI=http://localhost:9000/admin/oauth/etsy/callback
ETSY_SCOPE=listings_r listings_w shops_r # Optional, has defaults
API Methods
OAuth
// Get authorization URL
const authUrl = await etsyService.getAuthorizationUrl(
redirectUri,
scope,
state
)
// Exchange code for token
const tokenData = await etsyService.exchangeCodeForToken(
code,
redirectUri
)
// Refresh expired token
const newTokenData = await etsyService.refreshAccessToken(
refreshToken
)
Shop Info
const shopInfo = await etsyService.getShopInfo(accessToken)
// Returns: { shop_id, shop_name, shop_url, currency, country, ... }
Listings
// Create listing
const listing = await etsyService.createListing(
accessToken,
shopId,
{
title: "Product Title",
description: "Product description",
price: 29.99,
quantity: 10,
tags: ["handmade", "vintage"],
}
)
// Update listing
const updated = await etsyService.updateListing(
accessToken,
listingId,
{ price: 24.99, quantity: 5 }
)
// Upload images
const images = await etsyService.uploadImages(
accessToken,
shopId,
listingId,
["https://example.com/image1.jpg", "https://example.com/image2.jpg"]
)
OAuth Integration
Unified OAuth Routes
The existing OAuth routes (/admin/oauth/[platform]) have been extended to support external stores:
Initiate OAuth
Endpoint: GET /admin/oauth/:platform
Supported platforms:
- Social:
facebook,instagram,twitter,linkedin,bluesky - Stores:
etsy,shopify,amazon
Response:
{
"location": "https://www.etsy.com/oauth/connect?...",
"state": "csrf_token_123"
}
Flow:
- Frontend calls
/admin/oauth/etsy - Backend generates authorization URL
- Frontend redirects user to Etsy
- User authorizes app
- Etsy redirects to callback URL with code
OAuth Callback
Endpoint: POST /admin/oauth/:platform/callback
Request Body:
{
"id": "etsy_account_id",
"code": "authorization_code_from_etsy",
"state": "csrf_token_123"
}
Response:
{
"success": true,
"account": { /* updated etsy_account record */ },
"shop_info": {
"shop_id": "12345",
"shop_name": "My Etsy Shop",
"shop_url": "https://www.etsy.com/shop/MyEtsyShop",
...
}
}
Flow:
- Frontend receives authorization code from Etsy
- Frontend posts to
/admin/oauth/etsy/callbackwith code and account ID - Backend exchanges code for access token
- Backend fetches shop information
- Backend updates
etsy_accountrecord with tokens and shop data - Frontend receives confirmation
Usage in Workflows
Resolving the Service
import { EXTERNAL_STORES_MODULE, ExternalStoresService } from "../../../modules/external_stores"
// In a workflow step
const externalStores = container.resolve(EXTERNAL_STORES_MODULE) as ExternalStoresService
// Get specific provider
const etsyProvider = externalStores.getProvider("etsy")
Example: Batch Sync Step
import { EXTERNAL_STORES_MODULE } from "../../../modules/external_stores"
import { ETSYSYNC_MODULE } from "../../../modules/etsysync"
export const batchSyncProductsStep = createStep(
"batch-sync-products-step",
async (input, { container }) => {
const externalStores = container.resolve(EXTERNAL_STORES_MODULE)
const etsysyncService = container.resolve(ETSYSYNC_MODULE)
// Get Etsy provider
const etsyProvider = externalStores.getProvider("etsy")
// Get account details
const [account] = await etsysyncService.listEtsy_accounts({
id: input.etsy_account_id,
})
for (const product_id of input.product_ids) {
try {
// Create listing on Etsy
const listing = await etsyProvider.createListing(
account.access_token,
account.shop_id,
{
title: productData.title,
description: productData.description,
price: productData.price,
quantity: productData.quantity,
images: productData.images,
}
)
// Update link with success
// ...
} catch (error) {
// Handle error
// ...
}
}
}
)
Adding New Store Providers
1. Create Service File
// src/modules/external_stores/shopify-service.ts
import { StoreProvider, TokenData, ShopInfo, ListingData, ListingResponse } from "./types"
export default class ShopifyService implements StoreProvider {
name = "shopify"
async getAuthorizationUrl(redirectUri: string, scope: string, state: string): Promise<string> {
// Implement Shopify OAuth
}
async exchangeCodeForToken(code: string, redirectUri: string): Promise<TokenData> {
// Implement token exchange
}
async refreshAccessToken(refreshToken: string): Promise<TokenData> {
// Implement token refresh
}
async getShopInfo(accessToken: string): Promise<ShopInfo> {
// Implement shop info fetch
}
// Add Shopify-specific methods
async createProduct(accessToken: string, productData: any) {
// Shopify product creation
}
}
2. Register Provider
// src/modules/external_stores/service.ts
import ShopifyService from "./shopify-service"
constructor(container: any, options?: any) {
super(...arguments)
this.registry = new StoreProviderRegistry()
this.registerProvider("etsy", new EtsyService())
this.registerProvider("shopify", new ShopifyService()) // Add here
}
3. Update OAuth Routes
// src/api/admin/oauth/[platform]/route.ts
const externalStorePlatforms = ["etsy", "shopify", "amazon"] // Add platform
4. Add Environment Variables
SHOPIFY_CLIENT_ID=...
SHOPIFY_CLIENT_SECRET=...
SHOPIFY_REDIRECT_URI=...
SHOPIFY_SCOPE=...
Integration with Etsy Sync
The etsysync module uses the external_stores module for all Etsy API interactions:
Data Flow
- OAuth →
external_storeshandles authentication - Token Storage →
etsysyncstores tokens inetsy_accountmodel - API Calls →
external_storesprovides Etsy API methods - Sync Logic →
etsysyncworkflows orchestrate the sync process
Module Separation
external_stores- Platform-agnostic store provider implementationsetsysync- Business logic for syncing products to external stores- OAuth routes - Unified authentication for both social and store platforms
Testing
Manual OAuth Test
-
Setup environment:
export ETSY_CLIENT_ID=your_keystring
export ETSY_CLIENT_SECRET=your_secret
export ETSY_REDIRECT_URI=http://localhost:9000/admin/oauth/etsy/callback -
Create etsy_account record:
INSERT INTO etsy_account (id) VALUES ('test_account_123'); -
Initiate OAuth:
curl http://localhost:9000/admin/oauth/etsy -
Visit returned URL in browser
-
After authorization, Etsy redirects to callback
-
Frontend posts to callback:
curl -X POST http://localhost:9000/admin/oauth/etsy/callback \
-H "Content-Type: application/json" \
-d '{
"id": "test_account_123",
"code": "code_from_etsy"
}' -
Verify account updated with tokens and shop info
Testing Listing Creation
const externalStores = container.resolve(EXTERNAL_STORES_MODULE)
const etsyProvider = externalStores.getProvider("etsy")
const listing = await etsyProvider.createListing(
accessToken,
shopId,
{
title: "Test Product",
description: "Test description",
price: 19.99,
quantity: 1,
}
)
console.log("Created listing:", listing.listing_url)
Error Handling
Common Errors
-
Missing Environment Variables
[EtsyService] Missing ETSY_CLIENT_ID or ETSY_CLIENT_SECRET→ Add environment variables
-
Invalid Authorization Code
Failed to exchange code for token: invalid_grant→ Code expired or already used, restart OAuth flow
-
Expired Access Token
Failed to create listing: Unauthorized→ Use
refreshAccessToken()to get new token -
Provider Not Found
Store provider "xyz" not found→ Check provider is registered in service constructor
Future Enhancements
Planned Features
-
Shopify Integration
- OAuth flow
- Product sync
- Inventory management
-
Amazon Integration
- MWS/SP-API authentication
- Listing management
- Order sync
-
eBay Integration
- OAuth 2.0
- Listing creation
- Auction management
-
Token Auto-Refresh
- Background job to refresh expiring tokens
- Automatic retry on 401 errors
-
Webhook Support
- Listen for store events
- Update local data on changes
-
Multi-Store Sync
- Sync same product to multiple stores
- Inventory synchronization across stores
API Reference
ExternalStoresService
class ExternalStoresService {
// Register a provider
registerProvider(name: string, provider: StoreProvider): void
// Get a provider (throws if not found)
getProvider(name: string): StoreProvider
// Check if provider exists
hasProvider(name: string): boolean
// List all registered providers
listProviders(): string[]
}
EtsyService
class EtsyService implements StoreProvider {
// OAuth
getAuthorizationUrl(redirectUri, scope, state): Promise<string>
exchangeCodeForToken(code, redirectUri): Promise<TokenData>
refreshAccessToken(refreshToken): Promise<TokenData>
// Shop
getShopInfo(accessToken): Promise<ShopInfo>
// Listings
createListing(accessToken, shopId, listingData): Promise<ListingResponse>
updateListing(accessToken, listingId, listingData): Promise<ListingResponse>
uploadImages(accessToken, shopId, listingId, imageUrls): Promise<any[]>
}
Summary
✅ Complete:
- External stores module with provider registry
- Etsy service with OAuth and API methods
- Unified OAuth routes for social + store platforms
- Type-safe interfaces for all providers
- Comprehensive error handling
⏳ Next Steps:
- Add Shopify provider
- Implement token auto-refresh
- Add webhook handlers
- Build admin UI for store management
The external_stores module provides a clean, scalable foundation for integrating with any external e-commerce platform, following established patterns from your social_provider module.