Skip to content
Draft
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
1 change: 1 addition & 0 deletions apps/api/src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ const configuration = {
webhook: {
authToken: process.env.SVIX_AUTH_TOKEN,
serverUrl: process.env.SVIX_SERVER_URL,
publicServerUrl: process.env.SVIX_PUBLIC_SERVER_URL || process.env.SVIX_SERVER_URL,
},
healthCheck: {
apiKey: process.env.HEALTH_CHECK_API_KEY,
Expand Down
11 changes: 9 additions & 2 deletions apps/api/src/webhook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ This service provides webhook functionality using [Svix](https://svix.com) as th
Set the following environment variables:

```bash
# Required: Your Svix authentication token
# Required: Your Svix authentication token.
SVIX_AUTH_TOKEN=your_svix_auth_token_here

# Optional: Custom Svix server URL (for self-hosted instances)
# Optional: Custom server-side Svix API URL for the Daytona API.
SVIX_SERVER_URL=https://your-svix-instance.com

# Optional: Browser-facing Svix API URL for the dashboard. Defaults to SVIX_SERVER_URL.
SVIX_PUBLIC_SERVER_URL=https://svix.example.com
```

For self-hosted Svix, use `SVIX_SERVER_URL` for the URL reachable by the Daytona API service. Set
`SVIX_PUBLIC_SERVER_URL` when browsers need a different public or proxied URL for `svix-react`; for example, API pods
may use `http://svix.svc.cluster.local:8071` while browsers use `https://svix.example.com`.

## API Endpoints

### Get App Portal Access
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/webhook/dto/webhook-app-portal-access.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0
*/

import { ApiProperty, ApiSchema } from '@nestjs/swagger'
import { ApiProperty, ApiPropertyOptional, ApiSchema } from '@nestjs/swagger'

@ApiSchema({ name: 'WebhookAppPortalAccess' })
export class WebhookAppPortalAccessDto {
Expand All @@ -18,4 +18,10 @@ export class WebhookAppPortalAccessDto {
example: 'https://app.svix.com/app_1234567890',
})
url: string

@ApiPropertyOptional({
description: 'The browser-facing Svix API URL for self-hosted Svix deployments',
example: 'https://svix.example.com',
})
serverUrl?: string
}
70 changes: 70 additions & 0 deletions apps/api/src/webhook/services/webhook.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2025 Daytona Platforms Inc.
* SPDX-License-Identifier: AGPL-3.0
*/

import { ServiceUnavailableException } from '@nestjs/common'
import { TypedConfigService } from '../../config/typed-config.service'
import { WebhookInitialization } from '../entities/webhook-initialization.entity'
import { WebhookService } from './webhook.service'
import { Repository } from 'typeorm'

describe('WebhookService', () => {
const organizationId = 'org_123'
const appPortalAccess = {
token: 'appsk_test',
url: 'https://app.svix.com/consumer/app_123',
}

const createService = (publicServerUrl?: string) => {
const configService = {
get: jest.fn((key: string) => (key === 'webhook.publicServerUrl' ? publicServerUrl : undefined)),
} as unknown as TypedConfigService

const service = new WebhookService(configService, {} as Repository<WebhookInitialization>)
const appPortalAccessMock = jest.fn().mockResolvedValue(appPortalAccess)
;(service as unknown as { svix: unknown }).svix = {
authentication: {
appPortalAccess: appPortalAccessMock,
},
}

return { service, appPortalAccessMock }
}

describe('getAppPortalAccess', () => {
it('returns token and url from Svix', async () => {
const { service, appPortalAccessMock } = createService()

const result = await service.getAppPortalAccess(organizationId)

expect(appPortalAccessMock).toHaveBeenCalledWith(organizationId, {})
expect(result.token).toBe(appPortalAccess.token)
expect(result.url).toBe(appPortalAccess.url)
})

it('includes serverUrl when browser-facing Svix URL is configured', async () => {
const { service } = createService('https://svix.example.com')

await expect(service.getAppPortalAccess(organizationId)).resolves.toEqual({
...appPortalAccess,
serverUrl: 'https://svix.example.com',
})
})

it('omits serverUrl when no Svix URL is configured', async () => {
const { service } = createService()

await expect(service.getAppPortalAccess(organizationId)).resolves.toEqual(appPortalAccess)
})

it('throws when Svix is not configured', async () => {
const service = new WebhookService(
{ get: jest.fn() } as unknown as TypedConfigService,
{} as Repository<WebhookInitialization>,
)

await expect(service.getAppPortalAccess(organizationId)).rejects.toBeInstanceOf(ServiceUnavailableException)
})
})
})
5 changes: 4 additions & 1 deletion apps/api/src/webhook/services/webhook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Organization } from '../../organization/entities/organization.entity'
import { InjectRepository } from '@nestjs/typeorm'
import { WebhookInitialization } from '../entities/webhook-initialization.entity'
import { Repository } from 'typeorm'
import { WebhookAppPortalAccessDto } from '../dto/webhook-app-portal-access.dto'

@Injectable()
export class WebhookService implements OnModuleInit {
Expand Down Expand Up @@ -220,16 +221,18 @@ export class WebhookService implements OnModuleInit {
/**
* Get Svix Consumer App Portal access for an organization
*/
async getAppPortalAccess(organizationId: string): Promise<{ token: string; url: string }> {
async getAppPortalAccess(organizationId: string): Promise<WebhookAppPortalAccessDto> {
if (!this.svix) {
throw new ServiceUnavailableException('Webhook service is not configured')
}
try {
const appPortalAccess = await this.svix.authentication.appPortalAccess(organizationId, {})
const publicServerUrl = this.configService.get('webhook.publicServerUrl')
this.logger.debug(`Generated app portal access for organization ${organizationId}`)
return {
token: appPortalAccess.token,
url: appPortalAccess.url,
...(publicServerUrl ? { serverUrl: publicServerUrl } : {}),
}
} catch (error) {
this.logger.debug(`Failed to generate app portal access for organization ${organizationId}:`, error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { queryKeys } from './queryKeys'
interface WebhookAppPortalAccess {
token: string
url: string
serverUrl?: string
}

export const useWebhookAppPortalAccessQuery = (organizationId?: string) => {
Expand Down
6 changes: 5 additions & 1 deletion apps/dashboard/src/providers/SvixProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ export function SvixProvider({ children }: SvixProviderProps) {
}

return (
<SvixReactProvider token={appPortalAccess.token} appId={initStatus.svixApplicationId}>
<SvixReactProvider
token={appPortalAccess.token}
appId={initStatus.svixApplicationId}
options={appPortalAccess.serverUrl ? { serverUrl: appPortalAccess.serverUrl } : undefined}
>
{children}
</SvixReactProvider>
)
Expand Down
5 changes: 5 additions & 0 deletions libs/api-client-go/api/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions libs/api-client-go/model_webhook_app_portal_access.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading