Skip to main content

App Structure

Apps extend O2VEND functionality by injecting code at specific hook points. This guide covers the app structure and organization.

App Overview

Apps allow you to:

  • Add custom functionality to themes
  • Create payment gateways
  • Integrate third-party services
  • Hook into theme rendering at specific points

App Location

Apps are stored in the tenant repository:

{tenant-repository}/apps/{app-id}/

For local development, apps can be placed in:

apps/{app-id}/

App Structure

An app consists of the following files:

{app-id}/
app.json # App metadata (required)
snippets/ # Liquid snippets for hooks (required)
{hook-name}.liquid
assets/ # Optional CSS/JS assets
app.css
app.js
gateway.js # Payment gateway implementation (for payment apps)

App Metadata (app.json)

The app.json file defines app metadata and configuration:

{
"id": "analytics-app",
"name": "Google Analytics",
"version": "1.0.0",
"description": "Adds Google Analytics tracking to the store",
"author": "Your Name",
"hooks": ["theme_head", "theme_body_end"],
"assets": ["app.css", "app.js"],
"priority": 100,
"settings": {
"tracking_id": {
"type": "text",
"label": "GA4 Measurement ID",
"default": ""
}
}
}

Required Fields

  • id (string): Unique app identifier (e.g., "analytics-app")
  • name (string): Display name (e.g., "Google Analytics")
  • version (string): Semantic version (e.g., "1.0.0")

Optional Fields

  • description (string): App description
  • author (string): Author or organization name
  • hooks (array): Array of hook names this app uses
  • assets (array): Array of asset filenames (CSS, JS)
  • priority (number): Execution priority (lower = higher priority, default: 100)
  • settings (object): App settings schema
  • payment_gateway (object): Payment gateway configuration (for payment apps)

Hook Snippets

Each hook the app uses requires a corresponding Liquid snippet file:

snippets/
theme_head.liquid
theme_body_end.liquid
product_price_after.liquid

Hook Snippet Example

{% comment %}
Hook: theme_head
App: analytics-app
{% endcomment %}

{% if settings.tracking_id %}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ settings.tracking_id }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ settings.tracking_id }}');
</script>
{% endif %}

App Assets

Assets (CSS, JavaScript) are stored in the assets/ directory:

assets/
app.css
app.js
images/
logo.png

Asset URLs

Assets are served via:

/apps/{tenant-id}/{app-id}/assets/{asset-name}

Example:

/apps/tenant123/analytics-app/assets/app.js

Referencing Assets

In hook snippets, reference assets using the app asset URL:

<link rel="stylesheet" href="/apps/{{ tenant.id }}/{{ app.id }}/assets/app.css">
<script src="/apps/{{ tenant.id }}/{{ app.id }}/assets/app.js"></script>

Payment Gateway Apps

Payment gateway plugins require a gateway.js file that exports a gateway implementation:

// gateway.js
module.exports = {
/**
* Initialize the payment gateway
* @param {Object} config - Gateway configuration
* @returns {Object} Gateway instance
*/
initialize(config) {
return {
name: 'Razorpay',
processPayment: async (paymentData) => {
// Payment processing logic
},
verifyPayment: async (paymentId) => {
// Payment verification logic
}
};
}
};

App Discovery

Apps are discovered via:

  1. API Endpoint (primary): GET /apps/enabled - Returns array of enabled app IDs
  2. Tenant Repository Scan (fallback): Scans tenant repository apps directory

An app is considered enabled if:

  • Listed in API response, OR
  • Present in tenant repository apps directory

App Context

Apps have access to the full Liquid context, including:

  • shop - Store information
  • product - Current product (on product pages)
  • cart - Shopping cart data
  • page - Current page information
  • tenant - Tenant configuration
  • settings - App-specific settings
  • All other theme variables

Accessing App Settings

{% if settings.tracking_id %}
<!-- Use setting -->
{% endif %}

App Priority

The priority field controls execution order when multiple apps use the same hook:

  • Lower priority = executes first
  • Default priority = 100
  • Higher priority = executes last

Example:

{
"id": "critical-app",
"priority": 10, // Executes first
"hooks": ["theme_head"]
}

Example App

Complete Analytics App

app.json:

{
"id": "google-analytics",
"name": "Google Analytics",
"version": "1.0.0",
"description": "Adds Google Analytics 4 tracking",
"hooks": ["theme_head", "theme_body_end"],
"assets": [],
"priority": 50,
"settings": {
"tracking_id": {
"type": "text",
"label": "GA4 Measurement ID",
"default": ""
},
"anonymize_ip": {
"type": "checkbox",
"label": "Anonymize IP",
"default": true
}
}
}

snippets/theme_head.liquid:

{% if settings.tracking_id %}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ settings.tracking_id }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ settings.tracking_id }}', {
{% if settings.anonymize_ip %}anonymize_ip: true{% endif %}
});
</script>
{% endif %}

snippets/theme_body_end.liquid:

{% if settings.tracking_id %}
<script>
gtag('event', 'page_view', {
'page_title': '{{ page.title | default: shop.name }}',
'page_location': window.location.href
});
</script>
{% endif %}

Local Development

For local development:

  1. Place app in /apps/{app-id}/ directory
  2. Use the same structure as tenant repository apps
  3. System will fallback to local apps if tenant repository is unavailable

Best Practices

  1. Unique IDs: Use descriptive, unique app IDs
  2. Versioning: Follow semantic versioning (major.minor.patch)
  3. Error Handling: Handle errors gracefully - app errors shouldn't break the theme
  4. Performance: Keep app snippets lightweight
  5. Documentation: Document app settings and usage
  6. Testing: Test apps with different themes and configurations
  7. Priority: Use priority to control execution order
  8. Settings: Provide sensible defaults for all settings

Next Steps