GuidesMigrateFrom Shopify

Migrate from Shopify

This guide maps Shopify’s model onto Flightdeck so you can plan a move with your eyes open. Some things transfer cleanly and immediately; some are operator-assisted today because the self-serve surface has not shipped yet. We name each one honestly rather than implying parity that does not exist.

The short version: Flightdeck’s strong, shipped story is consuming a provisioned store headlessly — you build a frontend against @dscodotco/sdk and place real orders through checkout.submit. Getting your catalog in is a scripted loop over the merchant API, not a one-click importer. Plan for that and the migration is straightforward.

If you have not read it yet, Build a storefront from scratch is the end-to-end tutorial for the consumer side. This page is about getting your Shopify data and concepts across; that tutorial is about building the store on top.

Concept mapping

ShopifyFlightdeckNotes
ProductProduct (slug, name, description, status)slug is unique per tenant and is the natural idempotency key on import.
VariantVariant (sku, price_cents, label, currency)Money is integer cents everywhere. label is the human option name (e.g. 5mg vial).
Options (Size / Color)Variant labelThere is no separate structured option-set model in the storefront shape today; the variant carries a single label. See Variants and options.
Collection (manual / smart)Collection (slug, title, description)Read-only over the SDK. Collection membership is operator-managed — there is no self-serve smart-rule engine.
Product imageContent-addressed asset (image_asset_ref, image_url)Uploaded via presign then attached; the storefront product carries a resolved image_url.
Storefront API tokenx-storefront-tokenAn opaque secret (no fixed prefix), provisioned by the platform operator; platform-wide today. Server-side only, checkout-authorizing. Consumer env var FLIGHTDECK_STOREFRONT_TOKEN.
Admin API tokenfdk_ merchant credentialAuthorization: Bearer, scope-bound (e.g. manage:own_catalog). Self-serve to mint (see Get your API keys).
Checkout (hosted / redirect)One-shot checkout.submitYou submit cart + card together to your own server; there is no hosted redirect. See below.
orders/create webhookorders.placed outbound webhookDelivered from the outbound webhook edge. See Webhooks.
Subscriptions app (Recharge etc.)Native subscriptions moduleSubscribe-and-save at checkout with recorded consent. See Add subscriptions.
Discount codescoupon_code at checkoutSee Discounts and coupons.
Gift cards / store creditgift_card_code, apply_store_creditBoth are checkout tenders; store credit has a balance read.

What transfers cleanly, and what is operator-assisted

Being blunt about the seams saves you a surprise mid-migration.

⚠️

Honest gap table. These are the areas where Flightdeck does not yet offer the self-serve surface a Shopify migrator expects. None of them block a migration; several of them mean an operator does a step for you today.

CapabilityState todayWhat you do instead
Bulk / CSV catalog importNot shippedRun the scripted create loop below. A native importer is planned.
Catalog writes from the SDKNot in the SDKCall the fdk_ merchant routes directly (raw fetch or the low-level client). The storefront SDK is read + checkout only.
Product status transition (draft to active)Create-time onlystatus is set when the product is created and defaults to active; there is no self-serve draft-to-active PATCH. Import as active after validating, or import as draft and coordinate going live with an operator.
Collection managementOperator-managedCollections are read-only over the SDK; membership and smart rules are set operator-side.
Storefront tokenOperator-provisionedThe storefront token is an opaque secret (no fixed prefix), provisioned by the platform operator; platform-wide today. Merchant fdk_ keys, by contrast, are self-serve — see Get your API keys.
Customer / password importNot self-serveShopper accounts are created on first sign-in; there is no bulk customer importer. Historical orders do not backfill.
Redirect / hosted checkoutNot offered by designYou own the payment-form UI in front of checkout.submit (see PCI note).

Orders and webhooks

On Shopify you would subscribe to orders/create. On Flightdeck the equivalent is the orders.placed outbound webhook, dispatched from the outbound webhook edge with an envelope that carries a required tenant_ref and is self-contained (a consumer never has to call back to use it). Point your fulfillment or ERP integration at that instead of orders/create. The full event catalog, envelope shape, and delivery/retry semantics are in Webhooks; order state values (placed, processing, shipped, refunded, …) are in Order lifecycle.

Checkout is one shot, not a redirect

This is the biggest behavioral difference from Shopify. There is no hosted checkout page and no redirect. Your server submits the cart and the card together in a single call, and the platform authorizes payment, runs the fraud floor, prices the authoritative total (shipping + tax), and places the order — or returns an honest decline.

// Server-side only — a Route Handler, server action, or edge function.
const result = await store.checkout.submit(
  {
    customer_ref: "guest:shopper@example.com",
    items: [{ variant_id: "var_123", quantity: 1 }],
    card: { ccnumber: "4111111111111111", ccexp: "12/28", cvv: "123" },
  },
  { idempotencyKey: `cart-${cartId}:attempt-1` },
);

Because your server touches raw card fields, that server is inside PCI-DSS scope (SAQ D territory) — the same fact of life as any non-hosted integration. Keep card data on the browser-to-your-HTTPS-handler-to-API path, never logged or stored. The full body shape, the tender order (store credit, then gift card, then card), the fraud floor, and how to read every outcome branch are in Checkout.

Importing your Shopify catalog

There is no bulk importer. The shipped, honest pattern is a scripted, idempotent loop over the merchant catalog API, keyed on the product slug so it converges on re-run instead of duplicating. These are fdk_ merchant routes under scope manage:own_catalog; the tenant is carried by the credential.

MethodPathPurpose
POST/v1/merchant/catalog/productsCreate one product.
POST/v1/merchant/catalog/products/:productId/variantsAdd a variant (SKU + price).
PUT/v1/merchant/catalog/products/:id/imageAttach an image ref.

Export from Shopify

Export your products from Shopify admin (Products, Export, CSV) or via the Admin API. You only need the fields Flightdeck accepts: a product needs a slug and a name; each variant needs a sku and a non-negative integer price_cents. Shopify prices are decimal strings ("49.99"), so convert to cents (4999) as you shape the rows — money is integer cents on this platform.

Shape the rows

type ImportRow = {
  slug: string;                     // Shopify "Handle" maps here — unique per tenant
  name: string;                     // Shopify "Title"
  description?: string;             // Shopify "Body (HTML)" — strip or keep as needed
  status?: "draft" | "active";      // set at create time only; default active
  variants: { sku: string; price_cents: number; label?: string }[];
};
 
// Shopify's decimal price string -> integer cents.
const toCents = (price: string) => Math.round(Number(price) * 100);
⚠️

status is set only at create time and defaults to active. There is no self-serve draft-to-active transition. Validate your rows first and import as active, or import as draft and coordinate going live with an operator.

Run the idempotent create loop

Run this server-side with an fdk_ key. A 409 slug_exists is treated as “already imported”, so a partial run is safe to re-run. Every failure is the nested envelope { error: { code, message } }; the loop stops on any code it does not recognize as an idempotent replay — a partial import never reports success.

const API = "https://api.ruo.pro";
const KEY = process.env.FLIGHTDECK_FDK_KEY!; // manage:own_catalog
 
async function call(method: string, path: string, body: unknown) {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  return { status: res.status, json: await res.json() };
}
 
for (const row of rows) {
  const created = await call("POST", "/v1/merchant/catalog/products", {
    slug: row.slug,
    name: row.name,
    description: row.description ?? null,
    status: row.status ?? "active",
  });
 
  let productId: string;
  if (created.status === 201) {
    productId = created.json.product.id;
  } else if (created.json.error?.code === "slug_exists") {
    // Imported on a previous run — look it up and continue.
    const found = await call("GET", "/v1/merchant/catalog/products", {});
    productId = found.json.products.find((p: any) => p.slug === row.slug).id;
  } else {
    throw new Error(`create failed for ${row.slug}: ${created.json.error?.message}`);
  }
 
  for (const v of row.variants) {
    const vres = await call("POST", `/v1/merchant/catalog/products/${productId}/variants`, {
      sku: v.sku,
      price_cents: v.price_cents,
      label: v.label ?? null,
      currency: "usd",
    });
    // A duplicate SKU is likewise refused, not duplicated — safe to re-run.
    if (vres.status !== 201 && vres.json.error?.code !== "sku_exists") {
      throw new Error(`variant ${v.sku} failed: ${vres.json.error?.message}`);
    }
  }
}

Move product images

Shopify image URLs do not transfer directly; Flightdeck imagery is a separate content-addressed upload. For each product image, follow the presign, PUT, finalize, attach flow and set the returned asset_ref with PUT /v1/merchant/catalog/products/:id/image. See Add an image to a product.

Verify before going live

Read the catalog back through the merchant surface and reconcile counts against your Shopify export before flipping the store live.

curl https://api.ruo.pro/v1/merchant/catalog/products \
  -H "Authorization: Bearer fdk_your_key_here"

For a disposable trial, POST /v1/merchant/catalog/demo-seed stocks a draft store with clearly-labeled sample products in one idempotent call — a good way to exercise the import shape before wiring your Shopify data. Full detail in Import an existing catalog.

A realistic migration order

Get your credentials from your operator

Ask your operator for a tenant ref and the storefront token (an opaque secret, no fixed prefix, provisioned by the platform operator; platform-wide today). The fdk_ merchant key scoped manage:own_catalog you mint yourself — merchant keys are self-serve (POST /v1/my/credentials/initial via an owner identity session, then POST /v1/my/credentials, subset-gated). See Get your API keys.

Import the catalog into a draft store

Run the loop above against a draft store so nothing serves publicly while you reconcile.

Build (or clone) the frontend

Either follow Build a storefront from scratch for a from-scratch Next.js store, or clone the Next.js Commerce fork for a ready-made one.

Wire fulfillment to orders.placed

Repoint whatever consumed Shopify’s orders/create at the orders.placed outbound webhook.

Coordinate go-live with your operator

Collections, going from draft to active, and domain verification are the operator-assisted steps. Line them up, then cut over DNS.