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
8 changes: 4 additions & 4 deletions openapi.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,7 @@
"type": "boolean"
},
"request_id": {
"description": "Echo of the X-Request-ID header for this request. Stable correlator agents can quote when emailing support@instanode.dev — saves the user from copy/pasting headers.",
"description": "Echo of the X-Request-ID header for this request. Stable correlator agents can quote when emailing contact@instanode.dev — saves the user from copy/pasting headers.",
"type": "string"
},
"retry_after_seconds": {
Expand Down Expand Up @@ -2217,8 +2217,8 @@
"description": "Recovery Point Objective in minutes — the maximum window of data loss a restore can incur. 0 means no backup/RPO guarantee for the tier.",
"type": "integer"
},
"rto_minutes": {

Check warning on line 2220 in openapi.snapshot.json

View workflow job for this annotation

GitHub Actions / typos

"rto" should be "to".
"description": "Recovery Time Objective in minutes — the target time to restore service after an incident. 0 means no RTO guarantee for the tier.",

Check warning on line 2221 in openapi.snapshot.json

View workflow job for this annotation

GitHub Actions / typos

"RTO" should be "TO".
"type": "integer"
},
"storage_limit_mb": {
Expand Down Expand Up @@ -3051,7 +3051,7 @@
},
"/api/v1/billing/change-plan": {
"post": {
"description": "Hobby ↔ Hobby Plus ↔ Pro on the same Razorpay subscription (upgrades only — downgrades are support-assisted). Proration is handled by Razorpay; the new plan takes effect at the end of the current billing period. The Team plan is NOT yet available for self-serve plan changes — target_plan=team returns 400 tier_not_yet_available (contact sales: support@instanode.dev).",
"description": "Hobby ↔ Hobby Plus ↔ Pro on the same Razorpay subscription (upgrades only — downgrades are support-assisted). Proration is handled by Razorpay; the new plan takes effect at the end of the current billing period. The Team plan is NOT yet available for self-serve plan changes — target_plan=team returns 400 tier_not_yet_available (contact sales: contact@instanode.dev).",
"requestBody": {
"content": {
"application/json": {
Expand Down Expand Up @@ -3103,14 +3103,14 @@
},
"/api/v1/billing/checkout": {
"post": {
"description": "Mints a Razorpay subscription for the requested plan (hobby, hobby_plus, or pro) tied to the authenticated team. The dashboard redirects the user to the returned short_url to complete payment; on success Razorpay fires subscription.activated AND subscription.charged to /razorpay/webhook — both trigger the same idempotent tier-elevation path so the team is upgraded as soon as the mandate is authorised, even before the first invoice is collected. The Team plan ($199, finite high-capacity limits — not unlimited) is NOT yet available for self-serve checkout — requesting plan=team returns 400 tier_not_yet_available (contact sales: support@instanode.dev). Capacity beyond the Team caps is Enterprise (contact sales). plan_frequency selects monthly (default) vs yearly billing — yearly returns 503 billing_not_configured until the operator creates the yearly Razorpay plan and sets RAZORPAY_PLAN_ID_*_YEARLY. promotion_code: admin-issued codes are bookmarked in the subscription notes for future discount wiring (no Razorpay Offer is applied yet — codes are not consumed until a real discount is confirmed). IDEMPOTENT: the endpoint never mints a second subscription for a team that already has a live one — if the team already holds the requested tier (or higher) it returns 400 already_on_plan, and if a prior checkout's subscription is still payable at Razorpay (status created/authenticated/pending) it returns that subscription's short_url with reused:true instead of creating a new one. This prevents a confused re-click from producing two parallel subscriptions that both charge the card.",
"description": "Mints a Razorpay subscription for the requested plan (hobby, hobby_plus, or pro) tied to the authenticated team. The dashboard redirects the user to the returned short_url to complete payment; on success Razorpay fires subscription.activated AND subscription.charged to /razorpay/webhook — both trigger the same idempotent tier-elevation path so the team is upgraded as soon as the mandate is authorised, even before the first invoice is collected. The Team plan ($199, finite high-capacity limits — not unlimited) is NOT yet available for self-serve checkout — requesting plan=team returns 400 tier_not_yet_available (contact sales: contact@instanode.dev). Capacity beyond the Team caps is Enterprise (contact sales). plan_frequency selects monthly (default) vs yearly billing — yearly returns 503 billing_not_configured until the operator creates the yearly Razorpay plan and sets RAZORPAY_PLAN_ID_*_YEARLY. promotion_code: admin-issued codes are bookmarked in the subscription notes for future discount wiring (no Razorpay Offer is applied yet — codes are not consumed until a real discount is confirmed). IDEMPOTENT: the endpoint never mints a second subscription for a team that already has a live one — if the team already holds the requested tier (or higher) it returns 400 already_on_plan, and if a prior checkout's subscription is still payable at Razorpay (status created/authenticated/pending) it returns that subscription's short_url with reused:true instead of creating a new one. This prevents a confused re-click from producing two parallel subscriptions that both charge the card.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"plan": {
"description": "Self-serve purchasable plans. The Team plan is NOT yet available for self-serve checkout (contact sales: support@instanode.dev) — plan=team returns 400 tier_not_yet_available.",
"description": "Self-serve purchasable plans. The Team plan is NOT yet available for self-serve checkout (contact sales: contact@instanode.dev) — plan=team returns 400 tier_not_yet_available.",
"enum": [
"hobby",
"hobby_plus",
Expand Down Expand Up @@ -5089,7 +5089,7 @@
"description": "Metrics fetched"
},
"400": {
"description": "invalid_id — :id is not a valid UUID — OR invalid_window — window param unparseable, non-positive, or > 7d hard maximum"

Check warning on line 5092 in openapi.snapshot.json

View workflow job for this annotation

GitHub Actions / typos

"unparseable" should be "unparsable".
},
"401": {
"description": "Unauthorized — session token required"
Expand Down
4 changes: 2 additions & 2 deletions public/docs/public/trust-residency.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Customer databases are backed up via snapshot. Snapshot retention by tier:
| Pro | 30 days |
| Team | 90 days |

Customer-initiated restore from a snapshot is rolling out for Pro and Team tiers. Until that surface is live, restore is operator-assisted — open a ticket at `support@instanode.dev` with the resource ID and target timestamp.
Customer-initiated restore from a snapshot is rolling out for Pro and Team tiers. Until that surface is live, restore is operator-assisted — open a ticket at `contact@instanode.dev` with the resource ID and target timestamp.

### Vault

Expand Down Expand Up @@ -79,7 +79,7 @@ We aim to say only what is true. Here is what is true today.
|---|---|
| SOC 2 Type II | In progress. Target completion Q3 2026. Audit firm not yet selected. We do not have a SOC 2 report to share today. |
| HIPAA | Not supported. We do not sign Business Associate Agreements today. If you need a BAA, email `enterprise@instanode.dev` so we can scope a Team-tier engagement and tell you whether and when we can support it. |
| GDPR | Standard Contractual Clauses (Module Two, controller-to-processor) are incorporated by reference in our [Data Processing Agreement](./dpa.md) — sign the DPA via `support@instanode.dev` to activate them. The product gating is separate: as of 2026-05, instanode.dev runs in NYC3 only, so customers whose users are EU residents should not route their PII through us without a separate residency commitment from the platform side. EU customers requiring an EU data-residency posture should wait for eu-west-1. |
| GDPR | Standard Contractual Clauses (Module Two, controller-to-processor) are incorporated by reference in our [Data Processing Agreement](./dpa.md) — sign the DPA via `contact@instanode.dev` to activate them. The product gating is separate: as of 2026-05, instanode.dev runs in NYC3 only, so customers whose users are EU residents should not route their PII through us without a separate residency commitment from the platform side. EU customers requiring an EU data-residency posture should wait for eu-west-1. |
| PCI-DSS | We do not handle cardholder data. Payment processing runs through Razorpay. Do not store card numbers in instanode.dev resources. |

If you are on a procurement call that requires a compliance answer not listed here, contact `security@instanode.dev` and we will tell you the truth instead of dodging.
Expand Down
4 changes: 2 additions & 2 deletions public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The `email` field must parse as a valid RFC 5322 address (validated via Go `mail

## Tiers

The public, self-serve tiers visible at `/pricing` are: Anonymous, Hobby, and Pro. Team is not yet a self-serve tier — it is available soon; contact support@instanode.dev for onboarding.
The public, self-serve tiers visible at `/pricing` are: Anonymous, Hobby, and Pro. Team is not yet a self-serve tier — it is available soon; contact contact@instanode.dev for onboarding.
Hobby Plus and Growth exist in `plans.yaml` as upsell-only intermediate tiers (reachable
via dashboard prompts when a Hobby user hits a wall) and are deliberately omitted from
the public tier ladder to keep the customer-facing comparison simple. Both still surface
Expand All @@ -101,7 +101,7 @@ on `/api/v1/capabilities` for agent introspection.

> **Upgrading auto-promotes in-flight deployment TTLs.** When a team upgrades to any paid tier (Hobby / Hobby Plus / Pro / Growth / Team), the Razorpay subscription.charged webhook flips the team's `default_deployment_ttl_policy` from `auto_24h` to `permanent` (so every future `POST /deploy/new` defaults to no TTL) AND promotes every existing `auto_24h` non-terminal deploy to permanent (clearing `expires_at`). Per-deploy `ttl_policy='custom'` and `ttl_policy='permanent'` rows are never touched — only the `auto_24h` class is rolled forward. To restore the 24h-default behaviour after an upgrade, `PATCH /api/v1/team/settings {"default_deployment_ttl_policy":"auto_24h"}`.
- **Pro**: $49/mo. 10 GB Postgres, 512 MB Redis, 5 GB Mongo, 50 GB storage, 10 apps. Resource-count cap: 5 active resources per service (redis is 3 — Redis RAM is the binding cost). Per-tier counts are introspectable via `resource_count_limit` on `/api/v1/capabilities`.
- **Team**: available soon — not yet self-serve. Planned at $199/mo with high finite limits (50 GB Postgres, 1.5 GB Redis, 40 GB Mongo, 40 GB queues, 300 GB storage, 30 GB vector, 100 deployments, 1000 vault entries, 100k webhooks), 50 custom domains, 90-day backups with self-serve restore, RBAC + audit log; SSO/SAML and a 99.9% SLA are also planned. Capacity beyond these caps (or dedicated/isolated infra, multi-region, or compliance such as SOC2/BAA/SSO/SLA/DPA) is Enterprise — contact sales. Team cannot be purchased or claimed today — contact support@instanode.dev for onboarding.
- **Team**: available soon — not yet self-serve. Planned at $199/mo with high finite limits (50 GB Postgres, 1.5 GB Redis, 40 GB Mongo, 40 GB queues, 300 GB storage, 30 GB vector, 100 deployments, 1000 vault entries, 100k webhooks), 50 custom domains, 90-day backups with self-serve restore, RBAC + audit log; SSO/SAML and a 99.9% SLA are also planned. Capacity beyond these caps (or dedicated/isolated infra, multi-region, or compliance such as SOC2/BAA/SSO/SLA/DPA) is Enterprise — contact sales. Team cannot be purchased or claimed today — contact contact@instanode.dev for onboarding.
- **Enterprise**: custom limits, dedicated infra, compliance; contact sales@instanode.dev. Not a self-serve tier and not in `plans.yaml` — no price, no checkout. Triggers: needs more than Team's caps, dedicated/isolated or multi-region infra, or SOC2/BAA/SSO/SLA/custom DPA.

## Conventions an LLM should follow when scripting against the platform
Expand Down
8 changes: 4 additions & 4 deletions src/api/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ export interface paths {
put?: never;
/**
* Switch the team's subscription to a different tier
* @description Hobby ↔ Hobby Plus ↔ Pro on the same Razorpay subscription (upgrades only — downgrades are support-assisted). Proration is handled by Razorpay; the new plan takes effect at the end of the current billing period. The Team plan is NOT yet available for self-serve plan changes — target_plan=team returns 400 tier_not_yet_available (contact sales: support@instanode.dev).
* @description Hobby ↔ Hobby Plus ↔ Pro on the same Razorpay subscription (upgrades only — downgrades are support-assisted). Proration is handled by Razorpay; the new plan takes effect at the end of the current billing period. The Team plan is NOT yet available for self-serve plan changes — target_plan=team returns 400 tier_not_yet_available (contact sales: contact@instanode.dev).
*/
post: {
parameters: {
Expand Down Expand Up @@ -526,7 +526,7 @@ export interface paths {
put?: never;
/**
* Create a Razorpay subscription and return its hosted-page URL
* @description Mints a Razorpay subscription for the requested plan (hobby, hobby_plus, or pro) tied to the authenticated team. The dashboard redirects the user to the returned short_url to complete payment; on success Razorpay fires subscription.activated AND subscription.charged to /razorpay/webhook — both trigger the same idempotent tier-elevation path so the team is upgraded as soon as the mandate is authorised, even before the first invoice is collected. The Team plan ($199, finite high-capacity limits — not unlimited) is NOT yet available for self-serve checkout — requesting plan=team returns 400 tier_not_yet_available (contact sales: support@instanode.dev). Capacity beyond the Team caps is Enterprise (contact sales). plan_frequency selects monthly (default) vs yearly billing — yearly returns 503 billing_not_configured until the operator creates the yearly Razorpay plan and sets RAZORPAY_PLAN_ID_*_YEARLY. promotion_code: admin-issued codes are bookmarked in the subscription notes for future discount wiring (no Razorpay Offer is applied yet — codes are not consumed until a real discount is confirmed). IDEMPOTENT: the endpoint never mints a second subscription for a team that already has a live one — if the team already holds the requested tier (or higher) it returns 400 already_on_plan, and if a prior checkout's subscription is still payable at Razorpay (status created/authenticated/pending) it returns that subscription's short_url with reused:true instead of creating a new one. This prevents a confused re-click from producing two parallel subscriptions that both charge the card.
* @description Mints a Razorpay subscription for the requested plan (hobby, hobby_plus, or pro) tied to the authenticated team. The dashboard redirects the user to the returned short_url to complete payment; on success Razorpay fires subscription.activated AND subscription.charged to /razorpay/webhook — both trigger the same idempotent tier-elevation path so the team is upgraded as soon as the mandate is authorised, even before the first invoice is collected. The Team plan ($199, finite high-capacity limits — not unlimited) is NOT yet available for self-serve checkout — requesting plan=team returns 400 tier_not_yet_available (contact sales: contact@instanode.dev). Capacity beyond the Team caps is Enterprise (contact sales). plan_frequency selects monthly (default) vs yearly billing — yearly returns 503 billing_not_configured until the operator creates the yearly Razorpay plan and sets RAZORPAY_PLAN_ID_*_YEARLY. promotion_code: admin-issued codes are bookmarked in the subscription notes for future discount wiring (no Razorpay Offer is applied yet — codes are not consumed until a real discount is confirmed). IDEMPOTENT: the endpoint never mints a second subscription for a team that already has a live one — if the team already holds the requested tier (or higher) it returns 400 already_on_plan, and if a prior checkout's subscription is still payable at Razorpay (status created/authenticated/pending) it returns that subscription's short_url with reused:true instead of creating a new one. This prevents a confused re-click from producing two parallel subscriptions that both charge the card.
*/
post: {
parameters: {
Expand All @@ -539,7 +539,7 @@ export interface paths {
content: {
"application/json": {
/**
* @description Self-serve purchasable plans. The Team plan is NOT yet available for self-serve checkout (contact sales: support@instanode.dev) — plan=team returns 400 tier_not_yet_available.
* @description Self-serve purchasable plans. The Team plan is NOT yet available for self-serve checkout (contact sales: contact@instanode.dev) — plan=team returns 400 tier_not_yet_available.
* @enum {string}
*/
plan: "hobby" | "hobby_plus" | "pro";
Expand Down Expand Up @@ -9332,7 +9332,7 @@ export interface components {
* @enum {boolean}
*/
ok: false;
/** @description Echo of the X-Request-ID header for this request. Stable correlator agents can quote when emailing support@instanode.dev — saves the user from copy/pasting headers. */
/** @description Echo of the X-Request-ID header for this request. Stable correlator agents can quote when emailing contact@instanode.dev — saves the user from copy/pasting headers. */
request_id?: string;
/** @description Seconds the agent should wait before retrying. null on 4xx (no retry — fix the request). int on transient 5xx: 30 for 503, 60 for 429, 10 for 502/504. For 429/502/503/504 the same value is also set in the Retry-After HTTP header. */
retry_after_seconds: number | null;
Expand Down
2 changes: 1 addition & 1 deletion src/components/ChangePlanModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ describe('ChangePlanModal — target tier rendering', () => {
it('always renders the downgrade-via-support exit path', () => {
render(<ChangePlanModal currentTier="hobby" onClose={() => {}} />)
const link = screen.getByTestId('change-plan-downgrade-support') as HTMLAnchorElement
expect(link.href).toContain('mailto:support@instanode.dev')
expect(link.href).toContain('mailto:contact@instanode.dev')
})

it('preselects defaultTargetTier when it is a valid (self-serve) upgrade', () => {
Expand Down
6 changes: 3 additions & 3 deletions src/components/ChangePlanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@

// Esc closes — matches TierChangeModal + the rest of the dashboard's
// modal-overlay convention. Suspended while a request is in flight so a
// mis-keyed Escape mid-Razorpay-call doesn't strand a half-submitted

Check warning on line 120 in src/components/ChangePlanModal.tsx

View workflow job for this annotation

GitHub Actions / typos

"mis" should be "miss" or "mist".
// change-plan call (the dashboard never sees the response).
useEffect(() => {
function onKey(e: KeyboardEvent) {
Expand Down Expand Up @@ -255,7 +255,7 @@
You're already on the highest plan available through self-serve.
To explore a custom plan,{' '}
<a
href="mailto:support@instanode.dev?subject=Plan%20change"
href="mailto:contact@instanode.dev?subject=Plan%20change"
style={{ color: 'var(--accent)' }}
>
contact support
Expand Down Expand Up @@ -354,7 +354,7 @@
<div style={{ marginTop: 6 }}>
Still stuck?{' '}
<a
href="mailto:support@instanode.dev?subject=Change%20plan%20failed"
href="mailto:contact@instanode.dev?subject=Change%20plan%20failed"
data-testid="change-plan-support-fallback"
style={{ color: 'var(--accent)' }}
>
Expand Down Expand Up @@ -396,7 +396,7 @@
who clicked "Change plan" hoping to downgrade aren't dead-
ended. Policy memory: downgrade is support-only. */}
<a
href="mailto:support@instanode.dev?subject=Downgrade%20plan"
href="mailto:contact@instanode.dev?subject=Downgrade%20plan"
data-testid="change-plan-downgrade-support"
style={{ fontSize: 11, color: 'var(--text-faint)' }}
>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/BillingPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ describe('BillingPage — initial render', () => {
expect(screen.queryByRole('button', { name: /cancel subscription/i })).toBeNull()
const link = screen.getByTestId('contact-support-cancel') as HTMLAnchorElement
expect(link.tagName).toBe('A')
expect(link.href.toLowerCase()).toContain('mailto:support@instanode.dev')
expect(link.href.toLowerCase()).toContain('mailto:contact@instanode.dev')
})
})

Expand Down Expand Up @@ -751,7 +751,7 @@ describe('BillingPage — cancellation is support-only', () => {
render(<BillingPage />)
await waitForLoaded()
const link = screen.getByTestId('contact-support-cancel') as HTMLAnchorElement
expect(link.href.toLowerCase()).toContain('mailto:support@instanode.dev')
expect(link.href.toLowerCase()).toContain('mailto:contact@instanode.dev')
})
})

Expand Down
Loading
Loading