Rails
The Ruby SDK 0.1.2 includes an executable Rails 8.1 reference application at samples/rails. It opens the existing on-site Lunixi form by default, with optional hosted checkout and server-calculated totals, stores its token encrypted, protects buyer routes with CSRF and demo authentication, and commits verified webhook receipts and order changes atomically.
This is a single-merchant fixed-total TRY example. It is a source integration, not a separately published Rails gem or a complete storefront. Request signing, token caching, retries and webhook cryptography remain in lunixi-sdk.
Install
Ruby 3.3+, Bundler, OpenSSL with Ed25519, and SQLite are required. Node.js is needed for the SDK's independent JSON compatibility tests.
git clone https://github.com/Lunixi-Payment/lunixi-ruby.git
cd lunixi-ruby
bundle installThe repository Gemfile loads the local SDK and installs Rails, Puma and SQLite. No RubyGems publication of lunixi-sdk is assumed. See the sample source (opens in a new tab).
Configure secrets
Export the following through your shell or secret manager before boot. The application does not auto-load dotenv; the repository includes samples/rails/.env.example as a checklist.
| Variable | Purpose |
|---|---|
LUNIXI_BASE_URL | HTTPS gateway origin, normally https://api-gateway.lunixi.com |
LUNIXI_KEY_ID | Registered merchant TEST/LIVE key id |
LUNIXI_MERCHANT_ID | Merchant UUID that must match the authenticated payment response |
LUNIXI_PRIVATE_KEY_PATH | Private Ed25519 key file outside the public web root |
LUNIXI_WEBHOOK_SECRET | Secret matching the registered webhook connection |
PUBLIC_ORIGIN | Public HTTPS origin of this Rails app, without a path |
DEMO_USERNAME, DEMO_PASSWORD | Demo HTTP Basic authentication; password at least 16 bytes |
SECRET_KEY_BASE | Stable Rails session secret |
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY | Stable primary encryption key |
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY | Independent deterministic encryption key |
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT | Independent derivation salt |
Generate a session secret locally with ruby -rsecurerandom -e 'puts SecureRandom.hex(64)'. Generate three additional independent 32-byte random values for encryption configuration. Save them securely and retain them across restarts. Plan Rails encryption key rotation before replacing any key; discarding an old key makes saved tokens unreadable.
A local HTTPS tunnel can supply PUBLIC_ORIGIN. The origin is used for the return URL and allowed-host validation. Configure it before launching the app. Do not publish a development server without authentication.
Start a payment
cd samples/rails
bundle exec rails db:migrate db:seed
bundle exec rails server -b 127.0.0.1Visit http://localhost:3000/orders/00000000-0000-4000-8000-000000000001, authenticate with the demo credentials and select Pay. The synthetic seeded order is 100.50 TRY. Add ?locale=tr for Turkish UI.
The checkout POST requires CSRF and uses persisted order totals. It commits an encrypted request/mode reservation before gateway access, then reuses the same key/body on retries. Default embedded mode renders the official Lunixi form in the merchant page; explicitly configured hosted mode validates and redirects to the saved backend paymentPageUrl with 303.
If the network fails, the order stays pending and retains its attempt key. Repeating checkout uses the same key. A persisted token is reused; the example never clears an uncertain attempt and silently creates a new charge. Expired or unknown attempts need explicit reconciliation.
The shared Lunixi form determines the methods available to that merchant, including eligible alternative methods and multi-card flows. The merchant application never handles PAN/CVV.
Register the webhook
Register this URL in the Lunixi dashboard and configure the matching webhook secret:
https://<your-public-host>/webhooks/lunixiSubscribe to the documented payment events. The external WebhooksController < ActionController::API uses exact request.raw_post bytes and no browser session. A separate middleware also accepts only POST /orders/{public_id}/return: it bounds the discarded raw body to 1 MiB, ignores every field, performs no order/session lookup, gateway request or financial write, and returns an empty fixed same-origin 303 to the existing authenticated GET. This informational adapter does not use the webhook secret. Checkout retains Rails CSRF and authentication; the normal GET still requires demo authentication, to be replaced with your customer/order authorization before production.
The SDK verifies v2 signature, timestamp, event id/type and rotation. RailsReplayStore then inserts a receipt with a unique (connection_id, event_id) constraint inside the same database transaction as order changes. An identical duplicate returns 200 without applying fulfillment twice. Different bytes under the same id return 400. Any handler/status-query failure rolls back the receipt and returns 503 so redelivery can recover.
Confirm settlement
WebhookProcessor acts on payment.captured only when the event identifies the stored order and intent. It authenticates payments.get(intent_id) and requires:
- final status
CAPTURED; - the same persisted order reference and intent;
- the same currency;
- the exact integer order amount.
Only then is the order marked paid. A failed, authorized, unknown or inconsistent result remains pending. Other subscribed events are recorded without inferring payment success. The browser return route only redirects to the persisted order state, ignoring client status or paid parameters.
The SDK supports split, capture and refund requests. This sample fulfils fixed-total single-intent TRY orders. Split aggregate settlement, partial fulfillment, customer-paid insurance and surcharges require an explicit accounting/reconciliation extension before they are enabled here. A mismatched total fails closed.
Adapt to your application
Replace demo HTTP Basic with your customer/session/tenant/order ownership checks. Preserve server-owned totals, persistent per-operation keys and atomic receipt/business transactions. Add a durable reconciliation worker for missing webhooks and uncertain outcomes, retaining the same correlation data.
The sample uses SQLite. To change the production database, install its Rails adapter and repeat transaction/concurrency checks. Long gateway reads inside database transactions simplify the example but limit throughput; a production outbox/state machine may use shorter transactions while preserving atomic event receipt and business updates.
HTTPS is forced outside local/test environments. Request info logging is disabled because Rails includes redirect URLs, which contain checkout credentials. Never log raw payment/webhook payloads or secrets. Use stable session/encryption secrets and a persistent database.
Tests and security
From the repository root:
bundle exec rake test rails:test
bundle exec rubocop
bundle exec bundle-audit check --update
bundle exec brakeman --force --rails8 --path samples/rails --no-pager
gem build lunixi-sdk.gemspecRails tests use synthetic transport responses and a real SQLite database; no merchant credentials or fund movement are required. They cover authentication, CSRF, encrypted tokens, durable keys, redirect injection, browser-return distrust, signatures, deduplication, wrong totals/order references and rollback/redelivery. Brakeman's single weak fixed-origin redirect false positive has a precise fingerprint, review rationale and attack regression tests in samples/rails/config/brakeman.ignore. No security category is disabled globally.
Before launch, run an authorized TEST-merchant end-to-end payment, webhook and refund acceptance test. See the repository README (opens in a new tab) and security policy (opens in a new tab).
On-site form and optional hosted checkout
The Rails example defaults to LUNIXI_CHECKOUT_MODE=embedded. It renders the existing Lunixi form on the merchant page using the fixed official CDN, HTML-escaped JSON data attributes and a per-response CSP nonce. Lunixi::Checkout.browser_config(token) provides only the official object/mount configuration. No gateway HTML or merchant secrets are rendered.
Set LUNIXI_CHECKOUT_MODE=hosted explicitly for optional redirect. Lunixi::Checkout.payment_page_url(data.fetch('paymentPageUrl')) and the Rails redirect wrapper validate the exact backend URL: HTTPS pay.lunixi.com/payment/ followed by a 16–4096-character ASCII opaque value, with no userinfo, port, query, fragment or controls. It can differ from the embedded token. Do not construct a URL from that token. Both modes expose the same merchant-enabled form methods, multi-card, installments and insurance; these features do not require redirecting. The existing fixed-total single-intent settlement limitation still applies.
Run rails db:migrate before deploying 0.1.2. New request/mode/callback and start time are persisted before network I/O; request/token/backend URL use Active Record Encryption. Retries reuse the saved body and key and stop financial dispatch after 23 hours. Existing rows are marked legacy hosted; unknown legacy requests or missing backend URLs remain reconciliation-only, without rewriting origin, resetting a key or inventing a URL. Global mode changes affect new reservations; existing requests retain their saved presentation. CSRF/demo authorization, raw webhooks, native receipt transactions and the bounded 3DS/split return adapter remain unchanged.
Focused 0.1.2 verification: 24 Rails form/return/URL/CSP security tests with 225 assertions, 2 SDK helper tests with 5 assertions, RuboCop and Brakeman (one exact documented strict-URL redirect false positive). Prior full 0.1.1 results remain the baseline; live/merchant acceptance is deferred.
The checkout page’s CSP permits form POSTs only to 'self' and the configured gateway origin (default https://api-gateway.lunixi.com). This preserves wallet/provider and legacy POST handoffs supported by the browser SDK. For another reviewed provider, set server-only LUNIXI_FORM_ACTION_ORIGINS to a comma-separated list of exact HTTPS origins, without spaces, paths, trailing slashes, credentials, query, fragment or wildcards. Invalid configuration fails closed; gateway responses and browser fields never expand this list. Script nonces, frame/connect restrictions and informational return handling remain in force.