Skip to content

Daily.co Integration

Daily.co provides videocall room management for appointments.

Features

  • Creates private rooms with max 2 participants
  • Room name format: restartix-{orgID}-{appointmentID}
  • Features: screenshare on, chat on, no prejoin UI
  • Auto-eject at expiration time
  • Create/update/delete room lifecycle

Go Client

go
// internal/integration/dailyco/client.go

type Client struct {
    httpClient *http.Client
    baseURL    string
    apiKey     string
}

func NewClient(apiKey, baseURL string) *Client {
    return &Client{
        httpClient: &http.Client{Timeout: 10 * time.Second},
        baseURL:    baseURL,
        apiKey:     apiKey,
    }
}

func (c *Client) CreateRoom(ctx context.Context, orgID, appointmentID int64, expiresAt time.Time) (*Room, error) {
    body := CreateRoomRequest{
        Name:    fmt.Sprintf("restartix-%d-%d", orgID, appointmentID),
        Privacy: "private",
        Properties: RoomProperties{
            MaxParticipants:   2,
            EnableScreenshare: true,
            EnableChat:        true,
            EnablePrejoinUI:   false,
            EnableNetworkUI:   false,
            EjectAtRoomExp:    true,
            Exp:               expiresAt.Unix(),
        },
    }

    resp, err := c.doRequest(ctx, http.MethodPost, "/rooms", body)
    if err != nil {
        return nil, fmt.Errorf("create room: %w", err)
    }

    var room Room
    json.NewDecoder(resp.Body).Decode(&room)
    return &room, nil
}

func (c *Client) UpdateRoom(ctx context.Context, orgID, appointmentID int64, expiresAt time.Time) error {
    name := fmt.Sprintf("restartix-%d-%d", orgID, appointmentID)
    body := UpdateRoomRequest{
        Properties: RoomProperties{Exp: expiresAt.Unix()},
    }
    _, err := c.doRequest(ctx, http.MethodPost, "/rooms/"+name, body)
    return err
}

func (c *Client) DeleteRoom(ctx context.Context, orgID, appointmentID int64) error {
    name := fmt.Sprintf("restartix-%d-%d", orgID, appointmentID)
    _, err := c.doRequest(ctx, http.MethodDelete, "/rooms/"+name, nil)
    return err
}

Cross-Tenant Isolation

Problem

Room names are restartix-{appointmentID}. If a user guesses another appointment's ID, they could attempt to join the room.

Mitigation: Meeting Tokens

Daily.co meeting tokens ensure participants don't join rooms by name — they use short-lived tokens that grant access to a specific room.

go
// Room name now includes org ID for namespacing
func roomName(orgID, appointmentID int64) string {
    return fmt.Sprintf("restartix-%d-%d", orgID, appointmentID)
}

func (c *Client) CreateRoom(ctx context.Context, orgID, appointmentID int64, expiresAt time.Time) (*Room, error) {
    body := CreateRoomRequest{
        Name:    roomName(orgID, appointmentID),
        Privacy: "private",
        Properties: RoomProperties{
            MaxParticipants:   2,
            EnableScreenshare: true,
            EnableChat:        true,
            EnablePrejoinUI:   false,
            EnableNetworkUI:   false,
            EjectAtRoomExp:    true,
            Exp:               expiresAt.Unix(),
        },
    }
    // ...
}

// Meeting tokens are generated per-participant with room scope
func (c *Client) CreateMeetingToken(ctx context.Context, roomName string, userID int64, expiresAt time.Time) (string, error) {
    body := CreateTokenRequest{
        Properties: TokenProperties{
            RoomName:  roomName,
            UserID:    fmt.Sprintf("%d", userID),
            Exp:       expiresAt.Unix(),
            IsOwner:   false,
        },
    }
    resp, err := c.doRequest(ctx, http.MethodPost, "/meeting-tokens", body)
    if err != nil {
        return "", fmt.Errorf("create meeting token: %w", err)
    }
    var result struct{ Token string `json:"token"` }
    json.NewDecoder(resp.Body).Decode(&result)
    return result.Token, nil
}

Videocall Access Flow

  1. Appointment created → room created as restartix-{orgID}-{appointmentID} (private)
  2. Patient/specialist requests videocall access → Go API verifies they own the appointment (RLS) → generates meeting token scoped to that room
  3. Client uses meeting token to join — no room name guessing possible
  4. Max 2 participants enforced by Daily.co

Security Rule

The Go API never returns the room name to the client. It only returns meeting tokens. The client SDK uses the token to join.

Business Associate Agreement (BAA)

Daily.co is a HIPAA-compliant service. Ensure a Business Associate Agreement is in place before using Daily.co in production for any PHI-related videocalls.

Testing

go
func TestDailyCoRoomIsolation(t *testing.T) {
    room, _ := dailyClient.CreateRoom(ctx, 1, 102, expiry)
    assert.Equal(t, "restartix-1-102", room.Name) // Includes org ID

    // Meeting token is scoped to specific room
    token, _ := dailyClient.CreateMeetingToken(ctx, room.Name, 42, expiry)
    assert.NotEmpty(t, token)
    // Token can only join "restartix-1-102", not "restartix-2-102"
}