An Instagram feed integration app, built the Near Native way.
NN Instagram is a Shopify app that syncs your Instagram Business feed and stores it as native Shopify metaobjects and files. This approach ensures:
- Data ownership: All Instagram posts are stored in the merchant's shop, accessible via the Shopify Admin
- Liquid accessibility: Posts can be queried and displayed directly in theme templates using Liquid
- Design flexibility: No widget limitations - style your Instagram feed however you want
- Performance: Images uploaded to Shopify Files CDN for fast loading
- Portability: Data stays in Shopify's standard format, no vendor lock-in
The app offers seamless Instagram integration where:
- Posts are stored in
nn_instagram_postmetaobjects - Feed configuration is stored in
nn_instagram_listmetaobjects - Images and videos are uploaded to Shopify Files for CDN delivery
- Manual sync keeps content up-to-date
- OAuth with Instagram Business API ensures secure, long-lived access
- Framework: React Router v7.9.3
- Database: Prisma + PostgreSQL (Prisma Accelerate)
- Language: TypeScript
- UI: Shopify Polaris
- API: Instagram Business API
- Deployment: Vercel
- Based on: Shopify App Template - React Router
Install NN Instagram from the Shopify App Store or via your partner dashboard.
- Open the app from Apps > NN Instagram in your Shopify admin
- Click Connect Instagram Account
- Log in with your Instagram Business or Creator account
- Authorize the app to access your Instagram Business account
- You'll be redirected back to the dashboard
- Click Sync Instagram Posts to import your Instagram content
- The app will:
- Fetch your recent posts from Instagram
- Upload images/videos to Shopify Files
- Create metaobjects for each post
- Store captions, likes, and comment counts
The app includes a Theme App Extension block that you can add to any page:
- Go to Online Store > Themes in your Shopify admin
- Click Customize on your active theme
- Navigate to any page (homepage, product page, etc.)
- Click Add block or Add section
- Look for NN Instagram Feed in the Apps section
- Add the block and customize settings:
- Number of posts to display (1-50)
- Aspect ratio (portrait or square)
- Border radius (0-48px)
- Gap between posts (0-64px)
- Padding (top/bottom, left/right)
- Show/hide profile header
- Show/hide Instagram handle
Each setting is fully customizable:
- Via Theme Editor: Adjust all visual settings in real-time
- Via Code: Edit the Liquid file in
extensions/instagram-feed/blocks/instagram-carousel.liquidfor complete HTML/CSS control - Via Theme CSS: Override styles using the
.nn-instagram-*CSS classes
- Node.js: ≥ 20.19 or ≥ 22.12 - Download
- Shopify Partner Account: Create an account
- Test Store: Set up a development store
- Instagram Business Account: Required for API access
- PostgreSQL Database: For production (Prisma Accelerate recommended)
npm install
npm run setup # Initialize Prisma
shopify app devPress P to open the URL to your app. Once you click install, you can start development.
-
Connect Instagram
- Authenticate with Instagram Business API
- App receives a 60-day long-lived access token
-
Sync Posts
- Manual sync imports recent posts
- Images/videos uploaded to Shopify Files
- Metaobjects created for each post
- Engagement data (likes, comments) stored
-
Configure Display
- Use the real-time preview in the dashboard
- Adjust visual settings (layout, spacing, colors)
- See changes instantly before applying to theme
-
Add to Storefront
- Install app block via theme editor
- Choose pages to display the feed
- Customize per-page if needed
-
Manage Connection
- Disconnect removes all synced data
- Reconnect to sync again
- Data is fully portable (stored in Shopify)
Access Instagram post data directly in your theme templates:
{% assign instagram_list = shop.metaobjects.nn_instagram_list.values | first %}
{% if instagram_list %}
{% assign instagram_posts = instagram_list.posts.value %}
<div class="instagram-feed">
<h2>Follow us on Instagram</h2>
<div class="instagram-grid">
{% for post_ref in instagram_posts limit: 12 %}
{% assign post = post_ref %}
{% assign post_data = post.data.value | parse_json %}
<div class="instagram-post">
{% if post.images.value.size > 0 %}
{% assign first_image = post.images.value.first %}
<img
src="{{ first_image | image_url: width: 600 }}"
alt="{{ post.caption.value | truncate: 100 }}"
loading="lazy"
>
{% endif %}
<div class="instagram-overlay">
{% if post.likes.value %}
<span class="likes">❤️ {{ post.likes.value }}</span>
{% endif %}
{% if post.comments.value %}
<span class="comments">💬 {{ post.comments.value }}</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}{% assign instagram_list = shop.metaobjects.nn_instagram_list.values | first %}
{% if instagram_list %}
{% assign latest_post = instagram_list.posts.value.first %}
{% assign post_data = latest_post.data.value | parse_json %}
<div class="featured-instagram">
<h3>Latest from Instagram</h3>
{% if latest_post.images.value.size > 0 %}
<img
src="{{ latest_post.images.value.first | image_url: width: 800 }}"
alt="Instagram post"
>
{% endif %}
{% if latest_post.caption.value %}
<p>{{ latest_post.caption.value | truncate: 200 }}</p>
{% endif %}
<div class="engagement">
<span>❤️ {{ latest_post.likes.value }} likes</span>
<span>💬 {{ latest_post.comments.value }} comments</span>
</div>
</div>
{% endif %}{% assign instagram_list = shop.metaobjects.nn_instagram_list.values | first %}
{% if instagram_list %}
<p>Connected to Instagram ✓</p>
{% else %}
<p>No Instagram feed connected</p>
{% endif %}Each Instagram post is stored as a metaobject with the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
| data | JSON | Yes | Full Instagram post data (raw API response) |
| images | File Reference List | No | Uploaded images/videos from the post |
| caption | Multi-line Text | No | Instagram post caption |
| likes | Number (Integer) | No | Number of likes |
| comments | Number (Integer) | No | Number of comments |
JSON Data Structure:
The data field contains the raw Instagram API response, including:
id: Instagram media IDmedia_type: "IMAGE", "VIDEO", or "CAROUSEL_ALBUM"media_url: Original Instagram media URLpermalink: Link to post on Instagramtimestamp: Publication timestampusername: Instagram username
A single list metaobject stores the feed configuration:
| Field | Type | Required | Description |
|---|---|---|---|
| data | JSON | Yes | Feed metadata and configuration |
| posts | Metaobject Reference List | No | List of nn_instagram_post references |
All images and videos are uploaded to Shopify Files for:
- CDN delivery (fast global loading)
- Shopify image transformations (automatic resizing, cropping)
- Theme compatibility
- No external dependencies
Access via {{ file | image_url: width: 600 }} in Liquid.
OAuth Flow:
- User clicks "Connect Instagram Account"
- Redirected to Instagram OAuth (
/instagram) - Instagram authorization screen
- Callback to
/instagram/callbackwith auth code - Exchange code for short-lived token
- Exchange short-lived for long-lived token (60 days)
- Fetch Instagram Business Account ID
- Store token in database
API Scopes:
instagram_business_basic- Read profile and postsinstagram_business_manage_messages- Future messaging featuresinstagram_business_manage_comments- Future comment moderationinstagram_business_content_publish- Future content publishinginstagram_business_manage_insights- Analytics and insights
Token Management:
- Long-lived tokens valid for 60 days
- Stored securely in PostgreSQL via Prisma
- Automatic refresh planned for future versions
When user clicks "Sync Instagram Posts":
-
Fetch posts from Instagram Business API:
GET /me/media?fields=id,media_type,media_url,permalink,caption,timestamp,username,children -
For carousel posts, fetch child media:
GET /{media_id}/children?fields=media_type,media_url -
Download images/videos from Instagram URLs
-
Upload to Shopify Files API:
mutation fileCreate($files: [FileCreateInput!]!) { fileCreate(files: $files) }
-
Create/update
nn_instagram_postmetaobjects:mutation metaobjectUpsert( $handle: MetaobjectHandleInput! $metaobject: MetaobjectUpsertInput! ) { metaobjectUpsert(handle: $handle, metaobject: $metaobject) }
-
Update
nn_instagram_listwith post references -
Return sync statistics to dashboard
model SocialAccount {
id String @id @default(cuid())
shop String
provider String // "instagram"
accessToken String // Long-lived Instagram token
userId String? // Instagram Business Account ID
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([shop, provider])
@@map("social_accounts")
}
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?
firstName String?
lastName String?
email String?
accountOwner Boolean @default(false)
locale String?
collaborator Boolean? @default(false)
emailVerified Boolean? @default(false)
}The app subscribes to Shopify webhooks:
app/uninstalled- Clean up data on uninstallapp/scopes_update- Handle scope changes- GDPR compliance webhooks:
customers/data_requestcustomers/redactshop/redact
| Endpoint | Method | Description |
|---|---|---|
/app/_index |
GET | Initial setup, creates metaobject definitions |
/app/dashboard |
GET | Main dashboard UI |
/app/dashboard |
POST | Handle sync/disconnect actions |
/instagram |
GET | Initiate Instagram OAuth |
/instagram/callback |
GET | Handle OAuth callback |
The Near Native approach gives you complete control over how the Instagram feed is displayed:
1. Edit Block File Directly
The theme block is a Liquid file you can edit:
extensions/instagram-feed/blocks/instagram-carousel.liquid
Modify the HTML structure, add custom Liquid logic, or completely redesign the layout.
2. Override Styles in Your Theme
The block uses namespaced CSS classes (.nn-instagram-*) that you can override in your theme's CSS:
/* In your theme's CSS file */
.nn-instagram-grid {
display: grid;
grid-template-columns: repeat(4, 1fr); /* Custom grid */
gap: 8px; /* Tighter spacing */
}
.nn-instagram-post {
border-radius: 0; /* Square posts */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.nn-instagram-post:hover {
transform: scale(1.05);
transition: transform 0.3s ease;
}3. Use Theme Settings
The block has configurable settings in the theme editor:
- Posts limit (1-50)
- Aspect ratio (portrait/square)
- Border radius (0-48px)
- Gap between posts (0-64px)
- Padding controls
- Header visibility
- Handle display
4. Build Completely Custom Implementations
Access Instagram data directly in any Liquid file:
{% assign instagram_list = shop.metaobjects.nn_instagram_list.values | first %}
{% assign posts = instagram_list.posts.value %}
<!-- Build your own custom layout -->
<div class="my-custom-instagram-slider">
{% for post in posts %}
<!-- Your custom HTML here -->
{% endfor %}
</div><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
<div class="swiper">
<div class="swiper-wrapper">
{% assign instagram_list = shop.metaobjects.nn_instagram_list.values | first %}
{% for post_ref in instagram_list.posts.value limit: 20 %}
{% assign post = post_ref %}
<div class="swiper-slide">
{% if post.images.value.size > 0 %}
<img
src="{{ post.images.value.first | image_url: width: 800 }}"
alt="{{ post.caption.value | truncate: 100 }}"
>
{% endif %}
<div class="post-caption">
{{ post.caption.value | truncate: 150 }}
</div>
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
new Swiper('.swiper', {
slidesPerView: 1,
spaceBetween: 16,
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
pagination: {
el: '.swiper-pagination',
},
breakpoints: {
640: { slidesPerView: 2 },
1024: { slidesPerView: 4 },
},
});
</script>The app is deployed on Vercel:
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel --prodEnvironment Variables:
Configure in Vercel dashboard:
DATABASE_URL- Prisma Accelerate connection stringSHOPIFY_API_KEY- From Shopify Partner dashboardSHOPIFY_API_SECRET- From Shopify Partner dashboardINSTAGRAM_APP_ID- From Facebook DevelopersINSTAGRAM_APP_SECRET- From Facebook DevelopersINSTAGRAM_REDIRECT_URI- Your Vercel deployment URL +/instagram/callbackINSTA_SCOPES- Instagram API scopes (comma-separated)
After deploying:
# Update app URLs and deploy extensions
npm run deployThis will:
- Update OAuth redirect URLs
- Register webhooks
- Deploy theme app extensions
For complete testing guide, see SHOPIFY_TESTING_INSTRUCTIONS.md.
Quick Test Checklist:
- Install app on development store
- Connect Instagram Business account
- Sync Instagram posts successfully
- Verify posts in Content → Metaobjects → NN Instagram Post
- Verify images in Content → Files
- Add app block to theme
- Configure display settings in theme editor
- Preview storefront - feed displays correctly
- Test responsive design (mobile/desktop)
- Test disconnect functionality
- Verify all data is deleted after disconnect
Issue: OAuth redirects but doesn't complete
Solutions:
- Verify
INSTAGRAM_REDIRECT_URImatches exactly in:.envfile- Facebook App settings
- Shopify app configuration
- Ensure Instagram account is a Business or Creator account
- Check that all required scopes are approved in Facebook App Review
Issue: Sync completes but no posts appear
Solutions:
- Verify Instagram Business Account is connected (not personal account)
- Check that account has published posts
- Verify API permissions in Facebook App dashboard
- Check token expiration date in database
Issue: Posts sync but images show broken
Solutions:
- Check Shopify Files in admin - images should be uploaded
- Verify
write_filesscope is granted - Check network tab for CORS or CDN issues
- Try re-syncing to re-upload images
Issue: Can't find app block in theme editor
Solutions:
- Run
npm run deployto deploy extensions - Verify theme is OS 2.0 compatible
- Check
extensions/instagram-feed/blocks/instagram-carousel.liquidexists - Clear browser cache and refresh theme editor
Issue: Sync fails with authentication error
Solutions:
- Disconnect and reconnect Instagram account
- Check
expiresAtinSocialAccounttable - Tokens last 60 days - reconnect before expiration
- Future versions will auto-refresh tokens
Planned features:
- Hashtag Filtering: Sync only posts with specific hashtags
- Story Support: Display Instagram Stories (24-hour availability)
- Reel Support: Enhanced video post display
- Comment Display: Show Instagram comments on posts
- Multi-Account: Support multiple Instagram accounts per store
- Analytics: Track which posts drive the most engagement
The Near Native brand builds apps that are as close to native Shopify functionality as possible. Key principles:
- Data storage: Always in shop-owner-accessible systems (metafields/metaobjects)
- External storage: Kept to an absolute minimum (only OAuth tokens)
- Design flexibility: Data accessible through Liquid templates
- Best practices: Proper linking throughout objects for easy data access
- Portability: Merchants own their data, no vendor lock-in
- Instagram Business API Documentation
- Metaobjects Documentation
- Shopify Files API
- Liquid Documentation
- React Router Shopify App Docs
- Theme App Extensions
This project is proprietary software. All rights reserved.
Mohamed Amezian
- GitHub: @mohamedamezian
- App: NN Instagram
Built with ❤️ for Shopify merchants following Near Native principles