A production-ready Shopify embedded app template built with Next.js 14+ App Router, TypeScript, Supabase, and TanStack Query. This starter follows all security best practices and provides a scalable architecture for building modern Shopify apps.
- β Shopify App Bridge v4 with secure token exchange authentication
- β Supabase for session/install storage (no Prisma, no Docker)
- β OOP Repository Pattern + Service Singleton for all external calls
- β TanStack Query hooks for client data fetching (no direct fetch in components)
- β Security-first: Strict CSP, no token logging, proper auth error handling
- β Webhooks: Server-side registration, APP_UNINSTALLED cleanup, GDPR handlers
- β GraphQL Codegen for type-safe operations
- β TypeScript throughout with full type safety
- β Shopify Polaris UI components
- β Production-ready build and deployment configuration
| Category | Package | Version | Purpose |
|---|---|---|---|
| Core Framework | next |
15.5.2 | Next.js App Router framework |
react |
^18.3.1 | React library (v18 for Polaris compatibility) | |
react-dom |
^18.3.1 | React DOM rendering | |
typescript |
^5 | TypeScript support | |
| Shopify Integration | @shopify/shopify-api |
^11.0.0 | Shopify API SDK with webhooks & auth |
@shopify/app-bridge-react |
^4.1.3 | App Bridge v4 React components | |
@shopify/polaris |
^12.0.0 | Shopify's design system components | |
| Database & Backend | @supabase/supabase-js |
^2.39.7 | Supabase client for database operations |
| State Management | @tanstack/react-query |
^5.28.6 | Server state management & caching |
| GraphQL | graphql |
^16.8.1 | GraphQL core library |
graphql-request |
^6.1.0 | Lightweight GraphQL client | |
@graphql-codegen/cli |
^5.0.2 | GraphQL code generation CLI | |
@graphql-codegen/client-preset |
^4.2.4 | Client-side code generation preset | |
@graphql-typed-document-node/core |
^3.2.0 | TypeScript support for GraphQL documents |
shopify-next-supabase-starter/
βββ π app/
β βββ π api/
β β βββ π hello/
β β β βββ route.ts # Example authenticated API route
β β βββ π webhooks/
β β βββ route.ts # Shopify webhook handler
β βββ π hooks/
β β βββ useGraphQL.ts # Generic type-safe GraphQL hook
β β βββ useSession.ts # Session management hook
β βββ π providers/
β β βββ providers.tsx # Main providers wrapper
β β βββ query-client.tsx # TanStack Query provider
β β βββ session-provider.tsx # Session management provider
β βββ layout.tsx # Root layout with Shopify metadata
β βββ page.tsx # Main embedded app page
β βββ globals.css # Global styles
βββ π lib/
β βββ π db/
β β βββ base-repository.ts # Base repository class
β β βββ session.repository.ts # Session data repository
β β βββ app-installation.repository.ts # App installation tracking
β β βββ service.ts # Database service singleton
β βββ π shopify/
β β βββ initialize-context.ts # Shopify API initialization
β β βββ verify.ts # Token verification utilities
β β βββ gdpr.ts # GDPR webhook handlers
β β βββ register-webhooks.ts # Webhook registration
β βββ π supabase/
β βββ server.ts # Supabase admin client
βββ π lib/gql/ # Generated GraphQL types (auto-generated)
βββ codegen.ts # GraphQL Codegen configuration
βββ next.config.ts # Next.js config with CSP headers
βββ .env.example # Environment variables template
βββ shopify.app.example.toml # Shopify CLI config template
βββ SETUP.md # Detailed setup instructions
Before you begin, ensure you have:
- Node.js 18+ and npm installed
- A Shopify Partner account (Sign up here)
- A Shopify development store (Create one here)
- A Supabase account (Sign up here)
- ngrok or similar tunneling service (Download here)
git clone https://github.com/diegolarraz/shopify-next-supabase-starter
cd shopify-next-supabase-starter
npm install# Copy environment templates
cp .env.example .env.local
cp shopify.app.example.toml shopify.app.toml-
Go to Shopify Partners Dashboard
- Visit partners.shopify.com
- Sign in or create an account
-
Create a New App
- Click "Apps" in the sidebar
- Click "Create app"
- Choose "Create app manually"
- Enter your app name and select "Embedded app"
-
Configure App URLs
- App URL:
https://your-ngrok-url.ngrok.io - Allowed redirection URLs:
https://your-ngrok-url.ngrok.io/api/auth/callback
- App URL:
-
Set App Scopes
- Go to "App setup" β "Configuration"
- Add required scopes (e.g.,
read_products,write_products)
-
Get Your Credentials
- Client ID (API Key): Found in "App setup" β "App info"
- Client Secret: Found in "App setup" β "App info"
Update your .env.local file:
# Shopify App Credentials
SHOPIFY_API_KEY=your_client_id_here
SHOPIFY_API_SECRET=your_client_secret_here
SCOPES=read_products,write_products
HOST=https://your-ngrok-url.ngrok.io
# Public variables (accessible in browser)
NEXT_PUBLIC_SHOPIFY_API_KEY=your_client_id_here
NEXT_PUBLIC_HOST=https://your-ngrok-url.ngrok.ioEdit shopify.app.toml:
name = "your-app-name"
client_id = "your_client_id_here"
application_url = "https://your-ngrok-url.ngrok.io"
embedded = true
[access_scopes]
scopes = "read_products,write_products"
[auth]
redirect_urls = [
"https://your-ngrok-url.ngrok.io/api/auth/callback"
]
[webhooks]
api_version = "2024-10"
[build]
automatically_update_urls_on_dev = true
dev_store_url = "your-dev-store.myshopify.com"-
Go to Supabase Dashboard
- Visit supabase.com
- Sign in or create an account
- Click "New project"
-
Configure Your Project
- Choose your organization
- Enter project name and database password
- Select a region close to your users
- Click "Create new project"
-
Go to Project Settings
- Click the gear icon in the sidebar
- Go to "API" section
-
Copy Required Values
- Project URL: Your project's API URL
- Service Role Key: The
service_rolesecret key (β οΈ Keep this secure!)
Update your .env.local file:
# Supabase Configuration
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_hereIn your Supabase dashboard, go to the SQL Editor and run this schema:
-- Session storage table
CREATE TABLE session (
id text PRIMARY KEY,
shop text NOT NULL,
access_token text,
scope text,
expires timestamptz,
is_online boolean DEFAULT false,
state text,
api_key text,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
-- Online access info for online tokens
CREATE TABLE online_access_info (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
session_id text UNIQUE REFERENCES session(id) ON DELETE CASCADE,
expires_in integer,
associated_user_scope text,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
-- Associated user information
CREATE TABLE associated_user (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
online_access_info_id uuid UNIQUE REFERENCES online_access_info(id) ON DELETE CASCADE,
user_id bigint,
first_name text,
last_name text,
email text,
account_owner boolean DEFAULT false,
locale text,
collaborator boolean DEFAULT false,
email_verified boolean DEFAULT false,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
-- App installation tracking
CREATE TABLE app_installation (
shop text PRIMARY KEY,
webhooks_registered boolean DEFAULT false,
installed_at timestamptz,
uninstalled_at timestamptz
);
-- Performance indexes
CREATE INDEX idx_session_shop ON session(shop);
CREATE INDEX idx_session_api_key ON session(api_key);
CREATE INDEX idx_app_installation_shop ON app_installation(shop);For additional security, you can enable RLS:
-- Enable RLS on all tables
ALTER TABLE session ENABLE ROW LEVEL SECURITY;
ALTER TABLE online_access_info ENABLE ROW LEVEL SECURITY;
ALTER TABLE associated_user ENABLE ROW LEVEL SECURITY;
ALTER TABLE app_installation ENABLE ROW LEVEL SECURITY;
-- Since we're using service role key, we can create policies that allow all operations
-- In production, you might want more granular policies
CREATE POLICY "Service role can do everything" ON session FOR ALL USING (true);
CREATE POLICY "Service role can do everything" ON online_access_info FOR ALL USING (true);
CREATE POLICY "Service role can do everything" ON associated_user FOR ALL USING (true);
CREATE POLICY "Service role can do everything" ON app_installation FOR ALL USING (true);In a terminal, start ngrok to create a secure tunnel:
# Install ngrok if you haven't already
# Visit https://ngrok.com/ for installation instructions
# Start ngrok on port 3000
ngrok http 3000Copy the https:// URL (e.g., https://abc123.ngrok.io) and update your environment variables and Shopify app configuration.
npm run devYour app will be available at http://localhost:3000 and accessible via your ngrok URL.
- Go to your Shopify Partner Dashboard
- Find your app and click "Test on development store"
- Select your development store
- Click "Install app"
# This should return 401/403 without proper auth
curl https://your-ngrok-url.ngrok.io/api/hello
# With proper Shopify session token, it should return 200
# (You'll get the session token from App Bridge in your embedded app)- Install your app on a development store
- Check your Supabase database - you should see entries in
app_installation - Uninstall the app
- Check the database - the
uninstalled_atfield should be populated
# Generate types from GraphQL operations
npm run graphql-codegenAll database operations use the repository pattern:
// Using the database service
import { dbService } from '@/lib/db/service';
// In an API route or server component
const session = await dbService.sessions.findById(sessionId);
const installation = await dbService.appInstallations.ensureWebhooksRegistered(shop);Always use React Query hooks in components:
// β Don't do this
const fetchData = async () => {
const response = await fetch('/api/data');
return response.json();
};
// β
Do this instead
import { useQuery } from '@tanstack/react-query';
function MyComponent() {
const { data, isLoading, error } = useQuery({
queryKey: ['my-data'],
queryFn: async () => {
const response = await fetch('/api/data');
return response.json();
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error occurred</div>;
return <div>{JSON.stringify(data)}</div>;
}- Create
.graphqlfiles in your app:
# app/graphql/products.graphql
query GetProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
}
}
}
}- Run codegen:
npm run graphql-codegen- Use the generated types:
import { useGraphQL } from '@/app/hooks/useGraphQL';
import { GetProductsDocument } from '@/lib/gql/graphql';
function ProductsList() {
const { data, isLoading } = useGraphQL(GetProductsDocument, { first: 10 });
if (isLoading) return <div>Loading products...</div>;
return (
<div>
{data?.products.edges.map(({ node }) => (
<div key={node.id}>{node.title}</div>
))}
</div>
);
}-
Connect Your Repository
- Go to vercel.com
- Import your GitHub repository
-
Configure Environment Variables
- Add all variables from
.env.local - Update
HOSTto your Vercel domain
- Add all variables from
-
Update Shopify App URLs
- Change app URL to your Vercel domain
- Update redirect URLs accordingly
-
Connect Your Repository
- Go to railway.app
- Create new project from GitHub
-
Configure Environment Variables
- Add all environment variables
- Railway will provide a domain automatically
- Shopify App Development
- Shopify App Bridge
- Shopify Admin API
- Next.js App Router
- Supabase Documentation
- TanStack Query
- Shopify Polaris
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
If you encounter any issues:
- Check the SETUP.md for detailed setup instructions
- Review the troubleshooting section
- Open an issue on GitHub with detailed information about your problem
Built with β€οΈ for the Shopify developer community