Medusa · Operate
Publish and deploy
- Confirm that the existing Garage
medusabucket exists and that its supplied key has read/write access. The manifests neither create nor change that bucket. Keepmedusa-s3,medusa-app,medusa-db-app,medusa-redis, andmedusa-oidcSealedSecrets in this application’s namespace. Preserve all keys on restore. - Build and publish the backend/Admin image with
.github/workflows/medusa-image.yaml. It reads the Node tag, Medusa version, image repository, and release tag fromapps/variables.yaml. The locked npm dependencies must matchMEDUSA_VERSION. Keep release tags immutable; increment the image tag for source/configuration changes. Ensure the package is readable by cluster nodes before reconciliation, or configure a pull Secret. Do not include runtime secrets in the image. - Publish the application sources and variables to
main, which the root Argo Application tracks. Argo’s rendering plugin discoversapps/medusa/app.yaml. Initial reconciliation can wait on the image build; resync once the image is published. Publishing before a successful build can leave Medusa unavailable. - Sync respects the declared waves: sealed credentials, CNPG, Redis/configuration, the Sync migration Job, then serving and worker Deployments. Migrations run once per sync before the deployments. They are not a PreSync hook. Existing processes can remain active during later migrations, so review schema compatibility and back up before upgrading.
- Verify Argo
Synced/Healthy, two CNPG instances, two Redis data members, three Sentinel voters, and two Ready pods in each Medusa Deployment. Verify certificate readiness and the real HTTPS/healthand/apppaths. The built-in/healthonly proves the process finished startup; separately test database writes, authenticated API access, background jobs, and S3 upload/download.
The expected backend hostname is medusa.antblu.net. DNS and edge routing must reach the shared Traefik ingress; a Certificate or Ingress alone does not prove reachability.
antID administrator login
The repository’s custom OIDC provider in apps/medusa/image/src/modules/antid/ is registered as antid, with display name antID. It uses the open-source openid-client library through Medusa’s custom-provider interface; it does not load Medusa’s licensed native OIDC provider. Admin offers both its email/password form and Continue with antID for the user actor. Existing Medusa local email/password credentials remain usable; Authentik passwords are not copied into Medusa. The custom /auth/emailpass/user route rejects public self-provisioning and email-based claiming of administrator accounts; local administrator creation uses invitations or operator provisioning. No custom Admin widget is needed.
Configure the existing Authentik Medusa confidential client with this exact redirect URI (including its query string):
https://medusa.antblu.net/app/login?auth_provider=antidThe issuer is https://auth.antblu.net/application/o/medusa/; Medusa discovers its authorization, token, and JWKS endpoints from the supplied discovery URL. Shared public values live under MEDUSA_OIDC_* in apps/variables.yaml. The medusa-oidc SealedSecret provides OIDC_CLIENT_SECRET to servers, workers, and migrations. An image rebuild is required for this configuration change; the configured release tag is incremented accordingly.
Bind the Authentik application to medusa-admins. The custom provider also requires that group in the validated OIDC claims on every login, before linking or provisioning an administrator. medusa-users alone cannot sign in as a Medusa Admin actor; storefront customers use their separate customer flow. Successful login can create a Medusa administrator automatically, so application access must be limited to store administrators. The custom /auth/user/antid/callback handler links an existing administrator when its normalized email matches the Authentik-managed email; otherwise it creates the user. Subsequent identity matching uses the stable OIDC sub claim and preserves the associated user ID. Authentik must return a nonempty email and stable sub, using the openid email profile scopes. MEDUSA_OIDC_REQUIRE_VERIFIED_EMAIL is explicitly false: this deployment trusts email addresses administered by Authentik and does not require the optional email_verified claim to be true. This policy also permits linking an existing administrator by that trusted email. Keep Authentik account/email administration and the Medusa application bindings restricted. The provider defaults to requiring verified email when this option is absent or set to true. First login does not require a separately bootstrapped local password.
The provider uses authorization code with S256 PKCE, nonce validation, issuer/audience/expiry checks, and JWKS signature verification. It accepts only the configured callback URL. Discovery is lazy, so building the image requires neither IdP access nor a client secret; authentication requires the runtime OIDC_CLIENT_SECRET. OAuth state expires after ten minutes and is consumed on callback through the Sentinel-backed legacy Redis cache required by the Auth module, alongside the existing modern caching provider, so a callback can reach a different server pod. The callback uses Medusa’s token-generation helper to retain verification and MFA checks. Losing Redis state during failover may require restarting login. Logging out of Medusa does not establish that the Authentik browser session has ended.
After publishing and deployment, verify that /auth/user/providers advertises antid with display name antID, the Admin button redirects to Authentik with the exact callback, and a permitted user returns to Admin successfully. Confirm that an identity denied by the Authentik application policy cannot sign in. An external interactive login is still required after deploying a changed image to prove the complete user transaction.
Google customer login
The storefront’s Google sign-in uses Medusa’s Google auth provider directly for the customer actor. The user actor remains limited to emailpass and antid; a Google customer login cannot provision an Admin user. Customers can still use email/password.
- In Google Cloud, create a Web application OAuth client and allow the storefront callback URL configured in
MEDUSA_CUSTOMER_GOOGLE_CALLBACK_URLas its exact authorized redirect URI. Complete any Google consent-screen requirements for the intended audience. - Seal a
medusa-googleSecret in namespacemedusawith keysGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET, using thesealed-secrets-controllercontroller in namespacesealed-secrets. Keep the plaintext inputs outside Git. The Secret references in the server, worker, and migration workloads are optional so an unconfigured installation remains startable; the provider registers only when both values exist. - Publish the new backend image tag and reconcile the Medusa Application. Restart the server and worker pods after adding or rotating the optional Secret because environment variables are read at process start.
- In the storefront deployment, set
COMMERCE_DEMO_MODE=false, configure a real Medusa publishable key, and setGOOGLE_CUSTOMER_LOGIN_ENABLED=trueonly after the provider is available. The storefront callback is server-side and writes the existing HttpOnly customer session cookie.
On first successful Google callback, the storefront creates the Medusa customer through the authenticated Store API and refreshes the token so it has a customer actor ID. Later Google logins reuse that identity. An existing email/password customer with the same email requires an explicit authenticated provider-linking flow; Google email alone must not claim that account. Verify first login, repeat login, /store/customers/me, and the existing Admin sign-in separately. An uncommitted image/configuration edit or a redirect to Google is not proof of a working customer session.
Login returns to Admin and fails with HTTP 401
A successful Authentik token exchange does not mean Medusa accepted the identity. Compare both server replicas’ callback logs with Authentik’s token-endpoint status. The callback preserves the provider’s credential-free failure message, including expired state, missing claims, and an unmet verified-email requirement.
On 2026-10-22, live failed logins had a successful Authentik token exchange but email_verified: false. The prior provider rejected those claims. The configured trust policy above permits these Authentik-managed addresses while retaining token signature, issuer, audience, expiry, PKCE, nonce, and state checks. Changing that policy requires publishing the configured backend image and reconciling the ConfigMap; source changes alone do not repair the deployed service.
Odoo catalog integration
The custom odoo module, sync-from-odoo workflow, and daily worker job follow Medusa’s Odoo recipe. Native Node fetch implements the same JSON-RPC protocol without another client dependency. The module uses Odoo 19’s standard search_read/read, default_code, and product_template_attribute_value_ids, avoiding optional/custom fields in the recipe. Odoo documents this RPC transport as deprecated; plan a transport migration before upgrading to a release that removes it.
This integration reads the catalog from Odoo into Medusa. It does not export orders or customers, import images, synchronize stock, map taxes, or evaluate customer pricelists. Prices use Odoo’s lst_price, including variant extras, in the product’s currency and Medusa v2 major units. Review tax inclusion and accepted currencies before selling imported products.
Provision the integration identity
Odoo must have the product catalog models installed, for example through Sales or Inventory, before synchronization can run. Account/key provisioning can precede that installation: the tool reports missing models and defers their read ACLs. It does not install or upgrade Odoo modules against serving replicas. Keep sync disabled while any required model is missing.
From the repository root, with cluster access, run this explicit account/key creation operation:
python3 apps/medusa/tools/provision-odoo-integration.pyThe tool creates medusa-integration and the Medusa Catalog Reader group. The account has no password and belongs only to this group, with read ACLs for installed product, variant, attribute, currency, company, and unit-of-measure models. It receives no Internal User, administrator, Sales, or Inventory role, and uses the Odoo shell’s current company. Existing company record rules apply. Effective create/write/delete permissions on those installed models are rejected before issuing a key. This account is separate from Authentik JIT provisioning.
After installing missing catalog modules through a separately planned Odoo maintenance operation, reconcile their read ACLs without generating or rotating the API key:
python3 apps/medusa/tools/provision-odoo-integration.py --reconcile-accessOn 2026-10-23 (America/Denver), provisioning created this dedicated account and persistent RPC key and wrote the local SealedSecret. Product templates, variants, attribute models, and units of measure were absent, so only the installed currency/company models received read ACLs. The credential was not deployed, sync remained disabled, and no product synchronization was performed.
It generates a persistent RPC-scoped API key and writes only encrypted values to apps/medusa/odoo-secret.yaml. The SealedSecret is bound to namespace medusa, name medusa-odoo, keys ODOO_USERNAME and ODOO_API_KEY, using sealed-secrets-controller in namespace sealed-secrets. Plaintext credentials cross only process pipes and memory. The public sealing certificate is fetched before creating the account; do not enable tracing or print captured subprocess output.
Provisioning refuses to overwrite a sealed file, claim an unrelated login, or rotate an existing API key. Odoo external IDs under medusa_integration identify its group and user. If Odoo commits a key but sealing subsequently fails, inspect the dedicated account’s API-key list privately and deliberately revoke the unusable key before retrying. Do not rotate credentials on pod startup. Back up Odoo’s identity/ACL/key-hash state and the SealedSecret with the sealing recovery material. To rotate, deliberately revoke the old key, retain the old sealed file as protected recovery material if needed, generate/seal its replacement, publish it, and restart both Medusa Deployments after reconciliation. Sync is interrupted until the replacement is loaded.
Activate and operate
MEDUSA_ODOO_SYNC_ENABLED defaults to false in apps/variables.yaml. medusa-env derives ODOO_URL from ODOO_HOST and ODOO_DB_NAME from ODOO_DATABASE_NAME. Servers, workers, and migrations reference the optional medusa-odoo Secret. Disabled integrations make no Odoo requests; enabling without all credentials fails startup. Module construction does not contact Odoo.
- Provision the account and sealed credential. Prepare the Medusa store’s default sales channel, a default shipping profile, and supported currencies; the integration creates none of these implicitly.
- Build and publish the configured immutable backend image. Publish the SealedSecret and set the enable flag to
true, following the normal Argo migration and rollout waves. Account creation alone does not deploy or activate sync. - The worker job runs daily at 00:00 UTC, with the container’s default UTC timezone (18:00 Denver daylight time or 17:00 standard time on the preceding day). For an immediate run, send an authenticated administrator
POSTwith an empty body tohttps://medusa.antblu.net/admin/odoo/sync. Normal Medusa Admin authentication applies. The response reportssynced; scheduled runs log start/completion counts. Large manual runs may outlast an HTTP proxy timeout; inspect workflow/worker state before retrying. - When checks are explicitly requested, verify authentication and read-only permissions, run a sync, compare IDs, options, prices, currencies, and publication status, and repeat it to confirm stable Medusa IDs. Assign inventory and media before testing a real storefront transaction. Readiness alone does not prove synchronization.
Templates, including archived templates, are scanned in ascending ID batches of 50. Product external_id is odoo:product.template:<id> with metadata.odoo_template_id; variants use metadata.odoo_variant_id. These IDs retain associations when SKUs change. Missing SKUs receive odoo-variant-<id>. Existing unlinked Medusa products are not adopted by matching a title or SKU. Newly created handles contain the template ID and initial name; later syncs preserve handles, categories, images, shipping profiles, sales-channel assignments, and unrelated metadata.
Odoo owns names, sale descriptions, SKUs, catalog prices, and publication status when the optional is_published field exists. Without that field, new imports are drafts and subsequent syncs preserve Medusa publication status. Archived or non-saleable templates become drafts. New variants enable inventory management and disallow backorders; assign Medusa inventory before sale. Existing inventory settings and quantities remain under Medusa control.
Removed variants, renamed options, or new option values on existing products stop the batch for explicit reconciliation in Medusa, avoiding silent deletion of inventory relationships. New variants using existing option definitions can be added. Deleted or newly inaccessible templates are not deletion instructions; previously imported products require explicit reconciliation. SKU conflicts and inaccessible attributes fail rather than merging another product.
Manual and scheduled runs share the existing Redis-backed, renewed distributed lock. A competing run fails to acquire it. Standard Medusa workflows provide compensation per batch; earlier completed batches remain after a later failure. A full rerun converges by external IDs. Redis failover can lose locks, and concurrent Odoo edits may require another run; this is neither exactly-once processing nor a cross-system snapshot. Disable the flag and roll the processes to stop future runs without deleting imported products. This describes source behavior; activation and end-to-end results require separate runtime evidence.
Recovery and failure drills
Confirm that CNPG routes medusa-db-rw to the current primary and that all Sentinels agree on one Redis master. Returning Redis members must follow that master. Check replication catch-up before declaring redundancy restored. Do not reset the PVCs to repair a role discrepancy.
Before a controlled worker-loss drill, obtain approval for the disruptive action. Prove an authenticated write and read plus a background job across promotion, then test the original primary’s return. Check S3 from Medusa using a dedicated test object; do not delete user objects. Store backup and restore evidence separately from the source-design claims.
The storefront is deferred. Set STORE_CORS and extend AUTH_CORS only when its actual origin is known. Configure and verify an externally reachable media URL and appropriate object read policy before relying on browser images. See the availability limits.
Optional customer integration module
The image includes a customer_sync module with versioned account legal evidence,
marketing decisions and a durable retry queue in its own Medusa-managed tables.
The migration runs through the existing predeploy database migration step.
CUSTOMER_SYNC_ENABLED gates external delivery and reconciliation, independently
of the catalog import flag. Configure the server-only Mautic API credentials,
Odoo contact writer, newsletter backend credential and policy versions before
enabling it. medusa-marketing is an optional service-owned Secret reference;
only encrypted SealedSecret manifests belong in source.
The customer account completion route requires explicit versioned acceptance; the stock Store customer creation endpoint is blocked to prevent bypass. Administrators/imports can still create business contacts without manufacturing legal or marketing acceptance. This changes the Store API contract for account registration: clients must use the new completion route. The optional integration does not change administrator authentication.
The worker uses supported Medusa customer/order/payment events, supported Mautic contact/field/segment/DNC APIs, and the existing Odoo JSON-RPC client. External service outages leave decisions queued; account creation and checkout do not synchronously call those services. Reconciliation repairs missed events and observes external contact/unsubscribe changes every fifteen minutes. Stable IDs and normalized email are used for matching; ambiguous records require review. Do not equate contact existence or customer segments with marketing consent.
Customer settings API
GET /store/account/settings reports only the authenticated customer’s Google
and password provider labels, legal/marketing state and integration delivery
state. POST accepts explicit operations: password, disconnect-google,
set-password, export, and close. Google-only password setup requires a
one-use proof from the actual Google callback, bound to its exact JWT and expiring
in five minutes; refresh tokens cannot manufacture proof. It adds authentication
to the existing customer and refuses provider-identity collisions. Bearer customer authentication is mandatory; browser
customer IDs are ignored. Sensitive requests share a distributed per-customer
lock and a fifteen-minute attempt budget. Password operations verify ownership
through a fresh email/password authentication. Email identifier changes require fresh credential proof and a code delivered to
the new address. The code is customer-bound, expires in ten minutes and permits
five guesses. A compensated workflow updates customer email, password-provider
identifier and integration identity, then queues the profile update. Conflicting
customers, auth identities or integration mappings stop the change.
Closing an account records a durable marketing withdrawal before soft-deleting
the customer and deleting its associated authentication identities. Password
accounts must confirm their current password and exact email. Google-only
accounts require a Google login within five minutes and exact email confirmation.
Store routes check that the customer and authentication identity still exist,
so an old JWT cannot retain access after closure. Financial records and Odoo
partners are retained. The worker suppresses marketing and removes customer
segment membership without rebuilding a closed customer’s profile. External
failures retry through the existing queue; CUSTOMER_SYNC_ENABLED=false means
external delivery remains disabled even when local preferences are saved.
The export allowlists profile, address, shopping-preference and consent fields. It includes order summaries but excludes authentication data, arbitrary metadata and internal IDs. It is a storefront export, not a complete response covering all personal data held by external systems. Full access/erasure requests, retention review and any further anonymization require the operator’s privacy process. Soft deletion is not an assertion that retained backups or financial records have been erased.
Publish the changed backend image and storefront together before exposing these controls. Verify password proof, cross-customer denial, Google linking, closed JWT denial, marketing opt-out delivery and retained accounting records using dedicated test identities. Source tests and browser fixtures do not prove live configuration or delivery.
Address synchronization now carries recipient name, phone, company and matched
country/state values to the existing Odoo child partner. Before deploying this
version, run the existing apps/medusa/tools/provision-customer-fields.py through
the operator workflow to add read-only res.country.state access. It retains
contact write/create rights, refuses unlink, and issues no credentials. No live
ACL reconciliation is implied by this source change.
Transactional account email
Account verification and recovery use a server-only SMTP transport with strict
certificate validation and mandatory TLS, independently of marketing consent.
Host, port, sender, username and storefront origin come from shared deployment
inputs; ACCOUNT_SMTP_PASSWORD comes from the service-owned
medusa-account-mail SealedSecret. Optional references allow an unconfigured
installation to start; the settings API reports email delivery unavailable.
Use python3 apps/medusa/tools/seal-account-mail.py to seal an existing SMTP
application password through a hidden prompt, or pass a protected local input
with --password-file. The tool refuses to overwrite an existing sealed file,
creates no mailbox credentials, and prints no plaintext. Preserve or revoke the
mail service credential through its owning account when rotating; do not use a
mail-server recovery/admin credential as the application’s SMTP password.
Password-reset requests use a generic response and identical event enqueue path for unknown and registered email addresses. The existing Redis-backed Medusa event bus delivers the email with retries; it does not use marketing DNC or segments. Links contain a 256-bit token in a browser-only fragment. Only its hash is cached; expiry is fifteen minutes, later requests supersede old links, and completion consumes the token under a distributed lock. Tokens/codes never enter Medusa notification records or application logs. Courtesy old-address alerts do not hold the shared customer-identity lock or roll back verified changes.
The reset screen and both storefront/backend releases must be published together. Confirm actual inbox delivery with a dedicated test account, expiry/replay rejection, identity ownership, and failure handling after reconciliation. A TLS connection or accepted SMTP envelope does not prove a received email.
Optional payment providers
The image supports the bundled Stripe provider and a local PayPal Orders v2 provider. Provider selection is controlled through MEDUSA_STRIPE_ENABLED and MEDUSA_PAYPAL_ENABLED in apps/variables.yaml; MEDUSA_PAYPAL_ENVIRONMENT selects sandbox or live, and MEDUSA_PAYPAL_AUTO_CAPTURE selects automatic capture after authorization. Enabling Stripe requires STRIPE_API_KEY and STRIPE_WEBHOOK_SECRET. PayPal requires PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET and PAYPAL_WEBHOOK_ID, with an HTTPS CUSTOMER_STOREFRONT_URL. Keep these in the optional medusa-payments Secret, sealed in this application’s directory. Server, worker and migration pods consume it. Missing credentials for an enabled provider fail startup.
Register signed processor webhooks at /hooks/payment/stripe_stripe and /hooks/payment/paypal_paypal, then enable the providers in supported Medusa regions. Both use authorization with subsequent capture. PayPal can automatically capture after authorization; Stripe remains configured for manual capture. The PayPal provider rejects unsupported currencies and amount changes that would reuse an approved order. It verifies webhook signatures with PayPal and correlates supported authorization, void, capture and denial events with the originating session. Refunds and voids run through Medusa; external refunds, disputes and reversals require separate reconciliation. Provider code and source settings do not establish live payment readiness. Publish an immutable image before selecting its new tag and verify the full payment lifecycle separately.
Optional commerce lifecycle outbox
The custom lifecycle module adds a migration-managed lifecycle_event table.
Order, capture, shipment, delivery, cancellation and confirmed-refund subscribers
queue independent email and ERP deliveries. The minute worker retries transient
failures. Enable TRANSACTIONAL_EMAIL_ENABLED and ODOO_ORDER_SYNC_ENABLED only
after configuring and testing the selected services. The authenticated
GET /admin/integrations/status reports queue counts and activation flags without
returning recipients or payloads. It is not an external-service health check.
SMTP cannot atomically commit acceptance and a local delivery receipt. An
interrupted/ambiguous SMTP send is held in review; reconcile the stable
Message-ID against the mail provider before changing its state. Never bulk-reset
sent or uncertain events. Keep the outbox during restores to prevent duplicate
notifications. ERP retries require the remote atomic bridge operation and its
unique order/event constraints.
POST /hooks/commerce/fulfillment accepts HMAC-SHA256 authenticated shipment and
delivery updates for an existing, explicitly mapped fulfillment. Sign the raw JSON
as timestamp + "." + body, send X-Commerce-Timestamp (Unix seconds) and
X-Commerce-Signature (hex), and configure FULFILLMENT_WEBHOOK_SECRET on both
ends. Requests expire after five minutes. The handler validates the order link,
uses Medusa workflows, and suppresses repeated lifecycle notifications. No
inventory ownership or carrier tracking state is inferred from this endpoint.
