Skip to content

feat(api): enable/disable destinations via Create and Update#895

Merged
alexluong merged 8 commits into
mainfrom
feat/destination-import-timestamps
May 5, 2026
Merged

feat(api): enable/disable destinations via Create and Update#895
alexluong merged 8 commits into
mainfrom
feat/destination-import-timestamps

Conversation

@alexluong
Copy link
Copy Markdown
Collaborator

@alexluong alexluong commented May 5, 2026

Summary

This PR bundles two related but distinct scopes.

1. Enable/disable via Create and Update (closes #488, #612)

  • Create accepts optional disabled_at so a destination can be created in a disabled state (e.g. importing a disabled Svix endpoint without a window where traffic could leak to it).
  • Update accepts optional disabled_at (omit / null / timestamp) so callers can enable/disable as part of a normal PATCH. The dedicated /enable and /disable endpoints stay — this is the import-friendly path.
  • Update emits an additional destination disabled / destination enabled audit log alongside destination updated when disability state changes.

2. Importable timestamps on Create

  • Create accepts optional created_at and updated_at so importers can preserve the original timestamps from the source system.
  • No validation on these fields — values are accepted as-is. Future timestamps and out-of-order timestamps are allowed; importers from other systems may carry inconsistent data and we'd rather accept it than block a migration.

Test plan

  • go test ./internal/apirouter/... — all pass
  • Manual: POST a destination with past created_at / disabled_at, verify timestamps preserved in response and storage
  • Manual: PATCH `disabled_at: null` on a disabled destination, confirm it enables and emits audit log

🤖 Generated with Claude Code

…eate; disabled_at on Update

Enables importing destinations from another system (e.g. Svix migrations) with
their original timestamps preserved. The Update API also accepts disabled_at
(omit/null/timestamp) so callers can enable/disable as part of a normal PATCH
without a separate /enable or /disable round-trip.

Validation: created_at <= updated_at <= now and created_at <= disabled_at <= now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexluong alexluong changed the title feat(api): accept import timestamps on destination Create/Update feat(api): enable/disable destinations via Create and Update May 5, 2026
alexluong and others added 2 commits May 5, 2026 18:45
… provided on Create

Importers can now send disabled_at alone without also having to send a
created_at; the destination's created_at silently mirrors disabled_at in
that case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trust import payloads — accept created_at/updated_at/disabled_at as-is
instead of enforcing future and ordering checks. Importers from other
systems may carry inconsistent data, and rejecting it would block
migrations for no real benefit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexbouchardd
Copy link
Copy Markdown
Contributor

Is the expected usage for importing a disabled destination to set disabled_at: current timestamp? I wonder if it might not be confusing in the case where disabled_at is in the future. Will it only take effect in the future or as long as a date is set it's disabled?

@alexluong
Copy link
Copy Markdown
Collaborator Author

I wonder if it might not be confusing in the case where disabled_at is in the future. Will it only take effect in the future or as long as a date is set it's disabled?

No, the current logic is if disabled_at exists -> take effect right away. That said, I reckon it's quite trivial to check the value of that timestamp compared to 'now'.

In what scenario would someone want to set a future timestamp for disabled_at?

@alexbouchardd
Copy link
Copy Markdown
Contributor

In what scenario would someone want to set a future timestamp for disabled_at?

I don't see the use case, but I'm pointing it out as a potential point of confusion if there's no validation

@alexluong
Copy link
Copy Markdown
Collaborator Author

We can add validation that disabled_at cannot be in the future?

@alexbouchardd
Copy link
Copy Markdown
Contributor

Yeah although that does bring up the question as to why created_at/updated_at could be in the future 🤔

The only validation worth keeping — a disabled_at in the future would
mean "scheduled to disable later," which is not a feature we support.
Other timestamp fields stay unvalidated; importers can carry whatever
their source system has.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexbouchardd
Copy link
Copy Markdown
Contributor

Aside we should probably limit created_at/updated_at input to API key auth?

Mirrors the disabled_at future check. None of these timestamps make
sense in the future at create time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexluong
Copy link
Copy Markdown
Collaborator Author

Yeah although that does bring up the question as to why created_at/updated_at could be in the future 🤔

fair 😅

Aside we should probably limit created_at/updated_at input to API key auth?

hmm, let me think, I don't believe we have a lot of logic that's limited to API key auth vs JWT, but I see the point.

alexluong and others added 3 commits May 5, 2026 19:30
JWT-authenticated tenants can no longer rewrite their own audit
timestamps; sending created_at or updated_at without admin auth
returns 403. disabled_at stays accessible to both roles, matching
the existing /enable and /disable endpoint semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Verify destination is not persisted when 403 is returned
- Cover both fields together
- Regression guard: JWT can still create destinations without these fields

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ErrorResponse.Parse leaves Code unset when it falls through to the
default branch (e.g. plain errors.New from handlers), which caused
AbortWithValidationError to emit {"status":0,...} alongside an HTTP
422. Set Code at the call site since AbortWithValidationError is
always 422 by definition; Parse stays caller-agnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexluong
Copy link
Copy Markdown
Collaborator Author

@alexbouchardd updated, ready for another round of review now!

@alexluong alexluong merged commit a1d10cc into main May 5, 2026
3 checks passed
@alexluong alexluong deleted the feat/destination-import-timestamps branch May 5, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow enabling/disabling a destination at creation

2 participants