Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions packages/docs/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> OpenIAP: Unified in-app purchase specification for iOS & Android
> Documentation: https://openiap.dev
> Quick Reference: https://openiap.dev/llms.txt
> Generated: 2026-08-01T17:31:39.495Z
> Generated: 2026-08-01T22:00:58.764Z

## Table of Contents
1. Installation
Expand Down Expand Up @@ -2024,6 +2024,12 @@ private state machine and retention policy.
IAPKit lives in the OpenIAP monorepo as a Bun + Hono server, Convex backend,
and React SPA deployed behind one origin.

The official hosted service is free under fair-use safeguards on shared,
community-funded capacity. It is best-effort, not unlimited or SLA-backed.
High-volume apps should contact hyo@hyo.dev before launch, help fund shared
capacity through GitHub Sponsors or OpenCollective, or self-host the
MIT-licensed server for dedicated capacity.

## API quick reference

Base URL: https://kit.openiap.dev
Expand Down Expand Up @@ -2056,7 +2062,7 @@ IAPKit does not relay those events through SSE, WebSockets, push, or long
polling. Apps persist only the user-scoped fields they need and conditionally
refresh on cold start, stale foreground, or explicit user action. Each refresh
still performs one mutation-free indexed Convex query so an expiry with no new
webhook is detected. Respect `429 Retry-After`, coalesce concurrent refreshes,
webhook is detected. Respect the `Retry-After` header on `429` and `503`, coalesce concurrent refreshes,
and enforce an app-defined maximum stale age for offline fallback. Secret-key
responses are `private, no-store` and omit `ETag`. The current
`kitApi.status()` and `kitApi.entitlements()` helpers are unconditional,
Expand Down Expand Up @@ -2130,15 +2136,29 @@ Harmonized `state` values (truthy `isValid`): `ENTITLED`,
- `403 INSUFFICIENT_SCOPE` — publishable key used for an administrative operation
- `403 INVALID_API_KEY` — wrong scheme or malformed key (format check only)
- `410 SECRET_API_KEY_IN_URL` — move the secret to `Authorization: Bearer ...` on the canonical route
- `429 RATE_LIMITED` — per-key bucket empty; honor `Retry-After` seconds
- `429 RATE_LIMITED` — API-key, source-IP, or process bucket empty; inspect `X-RateLimit-Scope` and honor `Retry-After`
- `503 SERVICE_BUSY` — the API-key, source-IP, or process verification share is full; inspect `X-Concurrency-Scope` and retry with jittered backoff
- `500 UNKNOWN_ERROR` — quote the `X-Correlation-Id` header in a support ticket

## Response headers (on 2xx / 4xx validation / 429)
## Response headers

- `X-Correlation-Id` — UUIDv4, matches the stdout log line
- `X-RateLimit-Limit` — bucket capacity (default 600 per key)
- `X-RateLimit-Remaining` — tokens left in the bucket
- `Retry-After` (429 only) — seconds to wait
- `X-RateLimit-Scope` — rejecting `key`, source `ip`, or process `global` bucket on `RATE_LIMITED`
- `X-Concurrency-Limit` / `X-Concurrency-Remaining` — verification slots for the reported axis after the request reaches the in-flight guard
- `X-Concurrency-Scope` — `key`, trusted source `ip`, or process `global`
- `Retry-After` (429 / 503) — seconds to wait

Default protection is 600 burst / 10 req/sec per key, 600 / 5 req/sec
per source IP, 5,000 / 100 req/sec per process, 8 concurrent verify handlers per
API key, 16 per trusted source IP, and 32 per process. The key and source shares
make simple credential rotation insufficient to monopolize the process from one
network source. Fly Proxy separately
limits the complete service to 80 soft / 120 hard concurrent requests per
machine. One million requests per day average about 11.6 req/sec before peaks,
so apps at that scale must coordinate capacity or self-host rather than
assuming DAU implies safe request volume.

## Docs

Expand All @@ -2149,7 +2169,7 @@ Harmonized `state` values (truthy `isValid`): `ENTITLED`,
- [/docs/verification/google](https://kit.openiap.dev/docs/verification/google) — package name, service account JSON
- [/docs/verification/horizon](https://kit.openiap.dev/docs/verification/horizon) — App ID + App Secret (write-only)
- [/docs/api](https://kit.openiap.dev/docs/api) — request shapes, responses, errors, headers, and Amazon RVS payloads
- [/docs/operations](https://kit.openiap.dev/docs/operations) — rate limits, logs, `/health`, graceful shutdown
- [/docs/operations](https://kit.openiap.dev/docs/operations) — fair use, capacity, rate and concurrency limits, logs, `/health`, graceful shutdown
- [openiap.dev/docs/webhooks](https://openiap.dev/docs/webhooks) — operator setup steps for inbound Apple ASN v2 and Google RTDN lifecycle delivery
- [/docs/ai-assistants](https://kit.openiap.dev/docs/ai-assistants) — how to point Codex / Claude / Cursor / etc. at this file
- [/docs/ai-assistants/codex-plugin](https://kit.openiap.dev/docs/ai-assistants/codex-plugin) — Codex plugin setup and self-hosted IAPKit MCP server option
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> OpenIAP: Unified in-app purchase specification for iOS & Android
> Documentation: https://openiap.dev
> Full Reference: https://openiap.dev/llms-full.txt
> Generated: 2026-08-01T17:31:39.495Z
> Generated: 2026-08-01T22:00:58.764Z

## Installation

Expand Down
81 changes: 81 additions & 0 deletions packages/docs/src/pages/docs/kit-backend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,87 @@ async function refreshEntitlements(
</div>
</section>

<section>
<AnchorLink id="hosted-capacity" level="h2">
Hosted capacity and high-volume apps
</AnchorLink>
<p>
The official hosted IAPKit service is open-source infrastructure
shared by the OpenIAP community. It is free under fair-use safeguards
and operated on a best-effort basis; it is not unlimited capacity and
does not include dedicated resources or an SLA.
</p>
<p>
Plan from request frequency and peak concurrency, not DAU alone. One
million users making one hosted request per day already averages about{' '}
<strong>11.6 requests per second</strong>, before cold-start,
release-day, or notification-driven peaks. That average exceeds the
hosted default per-key steady rate of 10 requests per second.
</p>
<p>
Hosted purchase verification also limits work already in progress to{' '}
<strong>
8 handlers per API key, 16 per trusted source IP, and 32 per process
</strong>
. The key and source shares make simple credential rotation
insufficient to monopolize the process from one network source;
requests beyond any axis receive <code>503 SERVICE_BUSY</code> instead
of entering an unbounded server queue.
</p>
<ul>
<li>
Verify after a purchase or restore; do not re-verify every receipt
on every app start.
</li>
<li>
Persist entitlement snapshots, coalesce concurrent refreshes, and
revalidate only when stale or explicitly requested by the user.
</li>
<li>
Honor the <code>Retry-After</code> header on <code>429</code> and{' '}
<code>503</code> with jittered backoff. A <code>304</code> saves
response transfer but still uses a Convex query invocation.
</li>
</ul>
<div className="alert-card alert-card--warning">
<p>
<strong>Contact us before a high-volume production launch.</strong>{' '}
If your organization expects to consume a meaningful share of hosted
capacity, we ask it to help fund server expansion, monitoring,
security, and load testing through{' '}
<a
href="https://github.com/sponsors/hyodotdev"
target="_blank"
rel="noopener noreferrer"
>
GitHub Sponsors
</a>{' '}
or{' '}
<a
href="https://opencollective.com/openiap"
target="_blank"
rel="noopener noreferrer"
>
OpenCollective
</a>
. Sponsorship supports shared capacity; it does not automatically
reserve dedicated resources or create an SLA.
</p>
<p>
For predictable capacity and full operational control,{' '}
<a
href="https://github.com/hyodotdev/openiap/tree/main/packages/kit#deployment-convex--flyio"
target="_blank"
rel="noopener noreferrer"
>
self-host the MIT-licensed server
</a>
. For capacity planning or a separate written arrangement, contact{' '}
<a href="mailto:hyo@hyo.dev">hyo@hyo.dev</a>.
</p>
</div>
</section>

<section>
<AnchorLink id="product-client-payloads" level="h2">
Product client payloads
Expand Down
117 changes: 116 additions & 1 deletion packages/docs/src/pages/sponsors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,87 @@ function Sponsors() {
</div>
</section>

<section className="resources-section">
<h2 style={{ textAlign: 'center' }}>
Keep Hosted IAPKit Shared and Sustainable
</h2>
<div
style={{
maxWidth: '760px',
margin: '0 auto',
textAlign: 'left',
lineHeight: '1.75',
color: 'var(--text-secondary, #666)',
}}
>
<p>
The official <code>kit.openiap.dev</code> service runs the
open-source IAPKit backend as shared infrastructure for the whole
ecosystem. It is free under fair-use safeguards, best-effort, and
intentionally available to developers who cannot operate a
receipt-validation server themselves.
</p>
<p>
If your organization expects sustained high volume or would use a
meaningful share of that capacity, we ask you to contact us before
launch and help fund the servers, monitoring, security, and load
testing your traffic requires. You can contribute through{' '}
<a
href="https://github.com/sponsors/hyodotdev"
target="_blank"
rel="noopener noreferrer"
style={{
color: 'var(--primary-color)',
textDecoration: 'underline',
}}
>
GitHub Sponsors
</a>{' '}
or{' '}
<a
href="https://opencollective.com/openiap"
target="_blank"
rel="noopener noreferrer"
style={{
color: 'var(--primary-color)',
textDecoration: 'underline',
}}
>
OpenCollective
</a>
.
</p>
<p>
Sponsorship strengthens shared capacity for everyone; it does not
automatically buy unlimited usage, dedicated resources, or an SLA.
Teams that need predictable scaling or full operational control
can{' '}
<a
href="https://github.com/hyodotdev/openiap/tree/main/packages/kit#deployment-convex--flyio"
target="_blank"
rel="noopener noreferrer"
style={{
color: 'var(--primary-color)',
textDecoration: 'underline',
}}
>
self-host the MIT-licensed server
</a>{' '}
or contact{' '}
<a
href="mailto:hyo@hyo.dev"
style={{
color: 'var(--primary-color)',
textDecoration: 'underline',
}}
>
hyo@hyo.dev
</a>{' '}
about a separate written arrangement.
</p>
</div>
</section>

<section className="resources-section">
<h2 style={{ textAlign: 'center' }}>
Why AI Can't Replace This Work
Expand Down Expand Up @@ -251,7 +332,8 @@ function Sponsors() {
>
GitHub Sponsors is the primary funding channel. Tiers scale from
individual contributors to companies shipping OpenIAP in
production — details are on the GitHub page.
production. OpenCollective is also available for transparent
community funding.
</p>
<div
style={{
Expand Down Expand Up @@ -304,6 +386,39 @@ function Sponsors() {
</svg>
<span>Sponsor on GitHub</span>
</a>
<a
href="https://opencollective.com/openiap"
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#3385ff',
color: '#ffffff',
padding: '0.9rem 2rem',
borderRadius: '0.5rem',
textDecoration: 'none',
fontWeight: '600',
fontSize: '1rem',
border: 'none',
transition: 'transform 0.2s, box-shadow 0.2s',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)',
minHeight: '54.72px',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow =
'0 4px 12px rgba(0, 0, 0, 0.18)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow =
'0 2px 8px rgba(0, 0, 0, 0.12)';
}}
>
<span>Support on OpenCollective</span>
</a>
<a
href="https://paypal.me/dooboolab"
target="_blank"
Expand Down
18 changes: 18 additions & 0 deletions packages/kit/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ CONVEX_DEPLOY_KEY=<your-convex-deploy-key>
# RATE_LIMIT_GLOBAL_CAPACITY=5000
# RATE_LIMIT_GLOBAL_REFILL_PER_SEC=100

# Expensive purchase-verification handlers allowed to wait on Convex or an
# upstream store at the same time in one process. Excess requests return
# 503 SERVICE_BUSY with Retry-After instead of being queued in memory.
# Default: 32. Keep this below the Fly proxy hard concurrency limit.
# VERIFY_MAX_IN_FLIGHT=32
# Maximum verification handlers one API key may occupy in this process.
# Default: 8. Keep this at or below VERIFY_MAX_IN_FLIGHT.
# VERIFY_MAX_IN_FLIGHT_PER_KEY=8
# Maximum handlers one trusted source IP may occupy. This makes API-key
# rotation insufficient to monopolize the process. Default: 16.
# Only Fly's `fly-client-ip` is trusted, so a deployment that does not run
# behind Fly resolves every caller to one shared "unknown" source. This axis
# then acts as a second process-wide cap: raise it to VERIFY_MAX_IN_FLIGHT to
# recover the intended global concurrency when self-hosting off Fly.
# VERIFY_MAX_IN_FLIGHT_PER_IP=16
# Retry hint for a full verification process. Default: 1 second.
# VERIFY_BUSY_RETRY_AFTER_SEC=1

# ────────────────────────────────────────────────────────────────
# Mixpanel (product analytics + retention dashboards at mixpanel.com).
# VITE_-prefixed → baked into the SPA bundle, so the value is public.
Expand Down
29 changes: 26 additions & 3 deletions packages/kit/COST-SAFETY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# IAPKit cost and abuse safety

This document records the cost model for the public IAPKit API as of July 28, 2026. It is an operational estimate, not an invoice forecast: actual Convex
This document records the cost model for the public IAPKit API as of August 2, 2026 in the project's Asia/Seoul timezone. It is an operational estimate, not an invoice forecast: actual Convex
database I/O depends on each project's document sizes and should be measured
from production function logs.

Expand Down Expand Up @@ -58,6 +58,16 @@ in-memory token bucket before calling Convex:
- payload catalog: a separate weighted limiter charges one token per requested
product, so a 50-item body page costs 50 tokens.

Fly Proxy limits the complete HTTP service to 80 soft / 120 hard concurrent
requests per machine. Inside the app, purchase verification has a stricter
default of 8 in-flight handlers per API key, 16 per trusted source IP, and 32
per process. The key and source shares prevent simple key rotation from
occupying every shared verification slot from one network source.
When any axis reaches its configured limit, IAPKit returns `503 SERVICE_BUSY` with
`Retry-After` and `X-Concurrency-Scope` instead of retaining request bodies and
sockets in an unbounded queue. The slot is released in a `finally` block on
both normal responses and downstream errors; idle key entries are deleted.

Stores use a 15-minute idle TTL and LRU eviction. Key and IP stores are capped
at 10,000 entries, so random-key/IP churn cannot grow process memory without
bound. Rejections return `429 RATE_LIMITED`, `Retry-After`,
Expand All @@ -73,6 +83,12 @@ cross-machine hard brake; a distributed globally consistent edge limit would
require additional infrastructure and is not justified for the current
single-machine deployment.

These controls reduce the blast radius of buggy clients, traffic spikes, and
common resource-exhaustion attempts. They are defense in depth, not a guarantee
that a public endpoint is immune to DDoS. Operators may still block abusive
sources at the platform edge and should review Fly and Convex telemetry during
an incident.

The Convex deployment URL is public configuration, and the legacy public
`subscriptionStatus` / `entitlements` functions remain callable with a
publishable key for rolling-deploy and rollback compatibility. Such direct
Expand Down Expand Up @@ -157,6 +173,12 @@ pathological ceiling is 201 million small row reads across those requests.
Rate limiting is the primary protection against an app polling continuously;
`304` responses do not make that request free.

One million requests spread evenly across one day average approximately 11.6
requests/second, already above the hosted default per-key steady rate of 10
requests/second before peak clustering. High-volume consumers must reduce call
frequency, coordinate shared capacity before launch, or self-host with limits
sized from measured latency and peak concurrency.

Pricing references:
[Convex pricing](https://www.convex.dev/pricing),
[Convex usage limits](https://docs.convex.dev/production/usage-limits), and
Expand Down Expand Up @@ -188,5 +210,6 @@ Monitor `function_execution` logs for `database_io_read_bytes`,
fetching payload catalogs, high `304`-free direct payload traffic, rate-limit
scope saturation, and unexpected nested purchase-verification calls.

No additional Fly machine, deployment, paid cache, or rate-limit service is
introduced by this feature.
No additional Fly machine, deployment, paid cache, or distributed rate-limit
service is introduced by these source guardrails. Increasing hosted capacity is
an explicit operational and funding decision rather than an automatic promise.
Loading