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
- Go to your app in the developer dashboard
- Update the App URL to your ngrok URL
- 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