Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt

Use this file to discover all available pages before exploring further.

Building Your First App

This guide walks you through building a complete LaunchMyStore app from scratch. By the end, you’ll have a working embedded app that can read products and display them in the admin.

What We’re Building

A simple “Product Insights” app that:
  • Authenticates via OAuth
  • Embeds in the LaunchMyStore admin
  • Fetches and displays product data
  • Uses App Bridge for native UI elements

Prerequisites

  • Node.js 18 or higher
  • A LaunchMyStore developer account
  • Basic knowledge of JavaScript/React

Part 1: Project Setup

Create the Project

mkdir product-insights-app
cd product-insights-app
npm init -y

Install Dependencies

npm install express dotenv @launchmystore/app-bridge
npm install -D nodemon

Project Structure

product-insights-app/
├── .env
├── package.json
├── server.js
├── public/
│   └── app.html
└── lib/
    └── session.js

Part 2: Configure Environment

Create a .env file:
LMS_CLIENT_ID=your_client_id
LMS_CLIENT_SECRET=your_client_secret
LMS_APP_URL=https://your-tunnel-url.ngrok.io
LMS_SCOPES=read_products,read_orders
PORT=3000
Use ngrok or Cloudflare Tunnel to expose your local server during development.

Part 3: Build the Server

server.js

require('dotenv').config();
const express = require('express');
const path = require('path');
const { saveSession, getSession } = require('./lib/session');

const app = express();
app.use(express.json());
app.use(express.static('public'));

const {
  LMS_CLIENT_ID,
  LMS_CLIENT_SECRET,
  LMS_APP_URL,
  LMS_SCOPES,
  PORT = 3000
} = process.env;

// OAuth: Start installation
app.get('/auth/install', (req, res) => {
  const { shop } = req.query;
  
  if (!shop) {
    return res.status(400).send('Missing shop parameter');
  }
  
  const state = Buffer.from(JSON.stringify({ shop, nonce: Date.now() })).toString('base64');
  
  const authUrl = new URL('https://api.launchmystore.io/oauth/authorize');
  authUrl.searchParams.set('client_id', LMS_CLIENT_ID);
  authUrl.searchParams.set('scope', LMS_SCOPES);
  authUrl.searchParams.set('redirect_uri', `${LMS_APP_URL}/auth/callback`);
  authUrl.searchParams.set('state', state);
  
  res.redirect(authUrl.toString());
});

// OAuth: Handle callback
app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;
  
  try {
    const { shop } = JSON.parse(Buffer.from(state, 'base64').toString());
    
    // Exchange code for tokens
    const tokenResponse = await fetch('https://api.launchmystore.io/oauth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: LMS_CLIENT_ID,
        client_secret: LMS_CLIENT_SECRET,
        code,
        grant_type: 'authorization_code',
        redirect_uri: `${LMS_APP_URL}/auth/callback`
      })
    });
    
    const tokens = await tokenResponse.json();
    
    if (tokens.error) {
      throw new Error(tokens.error_description || tokens.error);
    }
    
    // Save session
    await saveSession(shop, {
      accessToken: tokens.access_token,
      refreshToken: tokens.refresh_token,
      expiresAt: Date.now() + (tokens.expires_in * 1000),
      scope: tokens.scope
    });
    
    // Redirect to app
    const host = Buffer.from(`${shop}/admin`).toString('base64');
    res.redirect(`/app?shop=${shop}&host=${host}`);
    
  } catch (error) {
    console.error('OAuth error:', error);
    res.status(500).send('Authentication failed');
  }
});

// App entry point
app.get('/app', async (req, res) => {
  const { shop } = req.query;
  
  const session = await getSession(shop);
  if (!session) {
    return res.redirect(`/auth/install?shop=${shop}`);
  }
  
  res.sendFile(path.join(__dirname, 'public', 'app.html'));
});

// API proxy to fetch products
app.get('/api/products', async (req, res) => {
  const { shop } = req.query;
  
  const session = await getSession(shop);
  if (!session) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  
  try {
    const response = await fetch('https://api.launchmystore.io/api/v1/products', {
      headers: {
        'Authorization': `Bearer ${session.accessToken}`,
        'Content-Type': 'application/json'
      }
    });
    
    const data = await response.json();
    res.json(data);
    
  } catch (error) {
    console.error('API error:', error);
    res.status(500).json({ error: 'Failed to fetch products' });
  }
});

app.listen(PORT, () => {
  console.log(`App running on http://localhost:${PORT}`);
});

lib/session.js

Simple in-memory session storage (use a database in production):
const sessions = new Map();

async function saveSession(shop, data) {
  sessions.set(shop, data);
}

async function getSession(shop) {
  return sessions.get(shop);
}

async function deleteSession(shop) {
  sessions.delete(shop);
}

module.exports = { saveSession, getSession, deleteSession };

Part 4: Build the Frontend

public/app.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Product Insights</title>
  <script src="https://cdn.launchmystore.io/app-bridge.js"></script>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px; background: #f6f6f7; }
    .card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
    h1 { font-size: 20px; font-weight: 600; }
    .btn { background: #008060; color: white; border: none; padding: 10px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; }
    .btn:hover { background: #006e52; }
    .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; }
    .product { display: flex; gap: 12px; align-items: center; }
    .product img { width: 50px; height: 50px; object-fit: cover; border-radius: 6px; }
    .product-info h3 { font-size: 14px; font-weight: 500; }
    .product-info p { font-size: 12px; color: #6b7280; }
    .loading { text-align: center; padding: 40px; color: #6b7280; }
  </style>
</head>
<body>
  <div class="header">
    <h1>Product Insights</h1>
    <button class="btn" onclick="refreshProducts()">Refresh</button>
  </div>
  
  <div class="card">
    <div id="products" class="loading">Loading products...</div>
  </div>

  <script>
    // Initialize App Bridge
    const urlParams = new URLSearchParams(window.location.search);
    const host = urlParams.get('host');
    const shop = urlParams.get('shop');
    
    const app = window.LMSAppBridge.createApp({
      apiKey: 'YOUR_CLIENT_ID', // Replace with your Client ID
      host: host
    });
    
    // Show loading toast
    app.dispatch({
      type: 'LOADING_START'
    });
    
    // Fetch and display products
    async function loadProducts() {
      try {
        const response = await fetch(`/api/products?shop=${shop}`);
        const data = await response.json();
        
        app.dispatch({ type: 'LOADING_STOP' });
        
        if (data.error) {
          throw new Error(data.error);
        }
        
        renderProducts(data.products || []);
        
      } catch (error) {
        app.dispatch({
          type: 'TOAST',
          payload: { message: 'Failed to load products', isError: true }
        });
        document.getElementById('products').innerHTML = 'Failed to load products';
      }
    }
    
    function renderProducts(products) {
      const container = document.getElementById('products');
      
      if (products.length === 0) {
        container.innerHTML = '<p>No products found</p>';
        return;
      }
      
      container.className = 'product-grid';
      container.innerHTML = products.map(product => `
        <div class="product">
          <img src="${product.image?.src || 'https://via.placeholder.com/50'}" alt="${product.title}">
          <div class="product-info">
            <h3>${product.title}</h3>
            <p>${product.variants?.[0]?.price || 'N/A'}</p>
          </div>
        </div>
      `).join('');
    }
    
    function refreshProducts() {
      app.dispatch({
        type: 'TOAST',
        payload: { message: 'Refreshing products...' }
      });
      loadProducts();
    }
    
    // Load products on page load
    loadProducts();
  </script>
</body>
</html>

Part 5: Run and Test

Start the Server

npx nodemon server.js

Expose with ngrok

ngrok http 3000

Update Your App Settings

  1. Go to your app in the developer dashboard
  2. Update the App URL to your ngrok URL
  3. Add the callback URL: https://your-ngrok-url/auth/callback

Install the App

Navigate to:
https://your-ngrok-url/auth/install?shop=your-store-slug

Next Steps

Add Extensions

Extend storefronts and checkout

Add Functions

Custom shipping and payment logic

Webhooks

React to store events in real-time

Billing

Monetize your app