Magento 2 API and Frontend in Go — HTML server side rendering, GraphQL and REST, one binary, no slow PHP.
GoGento Catalog connects your Magento 2 database to modern frontends via Echo and GORM. Single binary, ~300+ req/s on a single CPU, sub-30ms for 100 products with 100 EAV attributes. EAV flattening, stock, prices, categories. Concurrent-safe cache, extensible registry, standalone GraphQL mode. Works with Venia, React, Next.js, Vue.
Why GoGento? If you run Magento 2 and want a fast, headless API without PHP — deploy one binary, point it at your MySQL, and serve catalog data to PWA, mobile apps, or third-party integrations. No Magento runtime, no Composer, no PHP-FPM. Lower memory, faster cold starts, simpler ops. Use your existing Magento DB schema; products, categories, EAV attributes, stock, and prices flow through unchanged.
Architecture. Repository layer for DB access, service layer for logic, API handlers for HTTP. GraphQL schema matches Magento/Venia conventions so frontends built for Magento GraphQL work with minimal changes. REST flat endpoints return products with attributes as keys. Optional global in-memory cache for hot paths; disable with PRODUCT_FLAT_CACHE=off for direct DB. Cron jobs for background tasks. Extensible registry for custom resolvers and fields.
Deployment. Run full API (REST + GraphQL) or standalone GraphQL server. Single executable, config via env vars. systemd, Docker, or bare metal. No separate app server. Scale horizontally by running more instances behind a load balancer.
Set these in a .env file or your environment:
MYSQL_USER=magento
MYSQL_PASS=magento
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DB=magento
API_USER=admin
API_PASS=secret
REDIS_ADDR=""
REDIS_PASS=""
PORT=8080
MAGENTO_CRYPT_KEY=your_crypt_key # From app/etc/env.php for Realtime API auth
| Feature | Description | Doc |
|---|---|---|
| GraphQL API | Products, categories, search; Magento/Venia-compatible schema. Store header, pagination, filters. | graphql.md |
| REST API | Flat products (EAV as keys), orders CRUD. Basic auth. Optional store ID. | rest-api.md |
| Realtime API | Sub-30ms price/inventory lookups. HMAC-SHA256 signed. Raw SQL, parallel queries. | realtime-api.md |
| Standalone GraphQL | Run GraphQL only: go run ./cmd/graphql. No REST, smaller footprint. |
installation.md |
| EAV flattening | Attributes as keys, stock_item, index_prices. FetchWithAllAttributesFlat. | eav-products.md |
| Global cache | In-memory, concurrent-safe. ~300 req/s. Set PRODUCT_FLAT_CACHE=off to bypass. |
cache.md |
| Registry & cache | Global cache, registry, singleton repos. Per-request isolation. | registry.md |
| Product Import | Magmi alternative. Bulk CSV import with parallel EAV writes. ~127k products/min. | rest-api.md |
| Cron jobs | Scheduled and on-demand. go run cli.go cron:start --job Name. |
cron.md |
| Extending | Add entities, custom resolvers, GraphQL extensions. Tailwind. | extending.md |
| Testing | SQLite unit tests (no Magento needed) + MySQL integration tests. | testing.md |
cd gogento-catalog
go mod tidy
go run magento.go- GraphQL:
POST http://localhost:8080/graphql - Playground:
GET http://localhost:8080/playground
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Store: 1" \
-d '{"query":"query { products { total_count } }"}'Technical index · Installation · Production
GoGento includes a high-performance bulk product importer — a Go-native replacement for Magmi. It reads CSV files and writes directly to Magento's EAV tables using parallel bulk upserts, bypassing PHP entirely.
Magmi is a PHP-based direct-SQL importer that was fast for its era but has limitations:
- PHP memory ceiling — large catalogs (50k+ products) hit memory limits or require careful tuning
- Single-threaded — one EAV type at a time, no parallel DB writes
- Unmaintained — last meaningful update years ago, no Magento 2.4+ testing
- No API mode — CLI only, no REST endpoint for programmatic imports
GoGento's importer solves all of these:
- Parallel DB writes — EAV (varchar, int, decimal, text, datetime), stock, gallery, and price tables are flushed concurrently via goroutines
- Constant memory — streaming CSV parse + fixed batch buffers, memory doesn't grow with file size
- Raw SQL mode — optional
--raw-sqlflag bypasses ORM overhead for maximum throughput - Dual interface — CLI (
products:import) for files, REST API (POST /api/stock/import) for programmatic use - Single binary — no PHP, no Composer, no JVM; deploy and run
Each product has 50 EAV attributes (20 varchar, 10 int, 10 decimal, 5 text, 5 datetime).
| Metric | Value |
|---|---|
| Products imported | 100,000 |
| EAV rows upserted | 5,000,000 |
| Total time | 55.8 seconds |
| Throughput | 1,792 products/sec |
| Throughput | 107,493 products/min |
| EAV row rate | 89,578 rows/sec |
Tested on real Magento MySQL database (Community Edition schema). Performance on production systems may vary based on disk I/O, indexes, and connection pool settings.
| Metric | Value |
|---|---|
| Products imported | 100,000 |
| EAV rows upserted | 5,000,000 |
| Total time | ~47 seconds |
| Throughput | ~2,100 products/sec |
| Throughput | ~127,000 products/min |
| EAV row rate | ~106,000 rows/sec |
SQLite in-memory benchmark demonstrates Go-side processing overhead is minimal.
Tested on real Magento MySQL database with 1,000 existing products.
| Metric | Value |
|---|---|
| Products updated | 1,000 |
| Total time | ~156 ms |
| Throughput | ~6,400 products/sec |
| Throughput | ~385,000 products/min |
Stock import uses GORM's batch upsert (CreateInBatches with ON CONFLICT DO UPDATE) for efficient bulk updates with configurable batch size (default 500).
GoGento supports both Magento schema variants with automatic runtime detection:
| Edition | EAV Link Column | Detection |
|---|---|---|
| Community Edition (CE) | entity_id |
Default for SQLite and standard MySQL |
| Enterprise Edition (EE) | row_id |
Auto-detected from EAV table structure |
How Detection Works:
At startup, DetectSchema(db) queries DESCRIBE catalog_product_entity_varchar:
- If
row_idcolumn exists → EE schema, setsIsEnterprise = true - If
entity_idcolumn exists → CE schema, setsIsEnterprise = false
The Product struct contains both fields with json:"...,omitempty" tags, so JSON responses only include the relevant ID (zero values are omitted).
// EAVLinkID() returns the appropriate ID for EAV foreign keys
product.EAVLinkID() // Returns RowID for EE, EntityID for CENo configuration required — just connect to your database and GoGento adapts automatically.
# Import from CSV (raw SQL, batch 1000)
gogento products:import -f products.csv --raw-sql --batch-size 1000
# Stock import via API
curl -X POST http://localhost:8080/api/stock/import \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{"items": [{"sku": "SKU-001", "qty": 100, "is_in_stock": 1}]}'See rest-api.md for full CSV format, API request/response, and all available flags.
High-performance, stateless endpoints for real-time price and stock lookups with sub-30ms response times.
| Endpoint | Description |
|---|---|
GET /api/realtime/price-inventory?sku=X |
Price + stock (parallel fetch) |
GET /api/realtime/price?sku=X |
Lowest price only |
GET /api/realtime/stock?sku=X&source=default |
Stock quantity only |
GET /api/realtime/tier-prices?sku=X |
All tier prices |
- HMAC-SHA256 Authentication — Stateless, signed requests using Magento's crypt key
- Parallel Queries — Price and inventory fetched concurrently via errgroup
- Raw SQL — Direct
database/sqlqueries, no ORM overhead - Lowest Price Logic — Uses SQL
LEAST()across base price, special price, and tier price - Auto Schema Detection — Supports both CE (
entity_id) and EE (row_id)
// Generate HMAC signature
$signature = hash_hmac('sha256', $customerId, $cryptKey);
// Request with signed headers
$response = file_get_contents(
'http://gogento:8080/api/realtime/price-inventory?sku=SKU-001',
false,
stream_context_create([
'http' => [
'header' => "X-Customer-ID: {$customerId}\r\nX-Customer-Sig: {$signature}"
]
])
);// Signature should be generated server-side and passed to frontend
async function getRealtimePrice(sku, customerId, signature) {
const response = await fetch(
`https://gogento.example.com/api/realtime/price-inventory?sku=${encodeURIComponent(sku)}`,
{
method: 'GET',
headers: {
'X-Customer-ID': customerId,
'X-Customer-Sig': signature
}
}
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
// Usage
getRealtimePrice('SKU-001', '42', 'a1b2c3...')
.then(data => {
document.getElementById('price').textContent = `$${data.price}`;
document.getElementById('stock').textContent = data.stock > 0 ? 'In Stock' : 'Out of Stock';
});import { useState, useEffect } from 'react';
function useRealtimePrice(sku, customerId, signature) {
const [data, setData] = useState({ price: null, stock: null, loading: true });
useEffect(() => {
if (!sku) return;
fetch(`/api/realtime/price-inventory?sku=${encodeURIComponent(sku)}`, {
headers: {
'X-Customer-ID': customerId,
'X-Customer-Sig': signature
}
})
.then(res => res.json())
.then(json => setData({ price: json.price, stock: json.stock, loading: false }))
.catch(() => setData(prev => ({ ...prev, loading: false })));
}, [sku, customerId, signature]);
return data;
}
// Usage in component
function ProductPrice({ sku, customerId, signature }) {
const { price, stock, loading } = useRealtimePrice(sku, customerId, signature);
if (loading) return <span>Loading...</span>;
return (
<div>
<span className="price">${price?.toFixed(2)}</span>
<span className={stock > 0 ? 'in-stock' : 'out-of-stock'}>
{stock > 0 ? `${stock} in stock` : 'Out of stock'}
</span>
</div>
);
}<!-- Hyvä theme component using Alpine.js -->
<div x-data="realtimePrice('<?= $escaper->escapeJs($block->getSku()) ?>')"
x-init="fetchPrice()">
<span x-show="loading">Loading...</span>
<template x-if="!loading">
<div>
<span class="price" x-text="'$' + price?.toFixed(2)"></span>
<span :class="stock > 0 ? 'text-green-600' : 'text-red-600'"
x-text="stock > 0 ? stock + ' in stock' : 'Out of stock'">
</span>
</div>
</template>
</div>
<script>
function realtimePrice(sku) {
return {
sku: sku,
price: null,
stock: null,
loading: true,
customerId: '<?= $escaper->escapeJs($block->getCustomerId()) ?>',
signature: '<?= $escaper->escapeJs($block->getCustomerSignature()) ?>',
async fetchPrice() {
try {
const response = await fetch(
`<?= $escaper->escapeUrl($block->getGoGentoUrl()) ?>/api/realtime/price-inventory?sku=${encodeURIComponent(this.sku)}`,
{
headers: {
'X-Customer-ID': this.customerId,
'X-Customer-Sig': this.signature
}
}
);
const data = await response.json();
this.price = data.price;
this.stock = data.stock;
} catch (e) {
console.error('Realtime price fetch failed:', e);
} finally {
this.loading = false;
}
}
};
}
</script><?php
// Block class for Hyvä template
namespace Vendor\Module\Block;
class RealtimePrice extends \Magento\Framework\View\Element\Template
{
public function getCustomerSignature(): string
{
$customerId = $this->getCustomerId();
$cryptKey = $this->_scopeConfig->getValue('system/crypt/key');
return hash_hmac('sha256', (string)$customerId, $cryptKey);
}
public function getGoGentoUrl(): string
{
return $this->_scopeConfig->getValue('gogento/general/api_url') ?? 'http://gogento:8080';
}
}See realtime-api.md for full PHP client class, batch requests, and Magento Observer integration.
- With cache: ~300 req/s, ~1 ms single product (ApacheBench)
- 100 products, 100 attrs: REST ~25 ms, GraphQL ~30 ms (
make test-perf) - Realtime API: < 30ms price + inventory (parallel raw SQL)
- Product import: ~127,000 products/min (100k products, 50 attrs each, raw SQL)
- No N+1: batch Preload with IN clauses
MYSQL_USER=magento MYSQL_PASS=magento MYSQL_HOST=localhost MYSQL_DB=magento
API_USER=admin API_PASS=secret PORT=8080make test # All tests
make test-perf # GraphQL vs REST benchmark
