# How Account Linking Works Between Grok Web, Build, and Console in grok2api

> Discover how Grok Web, Build, and Console accounts link using association tables and PostgreSQL advisory locks for consistent cross-provider operations.

- Repository: [Chenyme/grok2api](https://github.com/chenyme/grok2api)
- Tags: internals
- Published: 2026-08-09

---

**Grok Web, Build, and Console accounts are linked through two many-to-one association tables—`account_provider_links` for Web-to-Build relationships and `web_console_account_links` for Web-to-Console relationships—enabling cross-provider operations with PostgreSQL advisory locks ensuring data consistency.**

The grok2api repository implements a multi-provider identity architecture where each service maintains distinct account records. Understanding how account linking works between Grok Web, Build, and Console is essential for administrators managing unified user identities and performing cross-platform operations.

## Data Model and Database Schema

### Provider-Specific Account Records

Each service maintains its own **provider account** records identified by the provider fields `grok_web`, `grok_build`, and `grok_console`. Rather than merging these into a single table, the system preserves separate records for each platform while establishing formal relationships through dedicated link tables.

### Association Tables

The relational persistence layer defines two critical junction tables in [`backend/internal/infra/persistence/relational/models.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/infra/persistence/relational/models.go):

- **`account_provider_links`**: Establishes Web-to-Build relationships with columns `web_account_id` and `build_account_id` (lines 92-100)
- **`web_console_account_links`**: Establishes Web-to-Console relationships with columns `web_account_id` and `console_account_id` (lines 102-108)

Both tables enforce **unique constraints** preventing a Web account from linking to multiple Build or Console accounts, and vice versa. The database schema includes check constraints (`chk_account_provider_links_distinct`) that maintain one-to-one cardinality between linked providers.

## The Account Linking Flow

The linking process follows a layered architecture ensuring transactional safety:

1. **API Endpoint**: The admin UI issues `POST` or `PUT` requests to `/api/admin/v1/accounts/:id/link`, handled in [`backend/internal/transport/http/account/handler.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/transport/http/account/handler.go)

2. **Service Layer**: The handler delegates to `AccountService.LinkWebToBuild` (or the Console equivalent) for business logic validation

3. **Repository Implementation**: `LinkWebToBuild` in [`backend/internal/infra/persistence/relational/account_repository.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/infra/persistence/relational/account_repository.go) executes the insert within a database transaction (lines 941-950)

4. **Concurrency Control**: A PostgreSQL advisory lock (`accountLinkLock`) serializes concurrent mutation attempts, preventing race conditions that could create duplicate links (tested in [`postgres_integration_test.go`](https://github.com/chenyme/grok2api/blob/main/postgres_integration_test.go))

5. **Referential Integrity**: Foreign-key constraints with `ON DELETE CASCADE` automatically remove link table rows when the parent Web account is deleted, maintaining database consistency without manual cleanup

## Retrieving Linked Accounts

When fetching account details via `GET /api/admin/v1/accounts/:id`, the system populates a `linkedAccounts` slice containing relationship metadata. The response structure defined in [`backend/internal/transport/http/account/handler.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/transport/http/account/handler.go) (lines 310-313) includes:

- `linkedAccountId`: The UUID of the paired account
- `linkedProvider`: The provider type (`grok_build` or `grok_console`)
- `linkedAccounts`: An array of objects containing `id`, `provider`, `name`, `email`, and `userId`

This allows admin interfaces to display complete relationship graphs without requiring additional API calls.

## Cascading Deletion of Linked Accounts

Account deletion supports optional cascading to linked peers through the `linkedDeleteTargets` parameter. When a request includes specific providers (e.g., `["grok_build","grok_console"]`), the system:

1. Parses targets using `parseLinkedDeleteTargets` in [`handler.go`](https://github.com/chenyme/grok2api/blob/main/handler.go) (line 1312)
2. Resolves linked IDs via `ResolveLinkedDeleteIDs` in [`backend/internal/repository/account.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/repository/account.go)
3. Expands the deletion scope to include one-hop linked accounts
4. Reports both `deleted` (root) and `linkedDeleted` (peer) counts in the response (lines 663-669)

This atomic operation ensures that removing a Grok Web account can simultaneously clean up associated Build and Console identities without orphaning records.

## Implementation Examples

### Linking a Web Account to a Build Account

The repository method handles the actual database insertion with transactional safety:

```go
// ctx – request context
// webID  – ID of the Grok Web account
// buildID – ID of the Grok Build account to link
err := accountsRepo.LinkWebToBuild(ctx, webID, buildID)
if err != nil {
    // handle ErrConflict (already linked) or other DB errors
}

```

This corresponds to the implementation in [`backend/internal/infra/persistence/relational/account_repository.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/infra/persistence/relational/account_repository.go) where the function inserts a row into `account_provider_links` while respecting the advisory lock mechanism.

### Querying Linked Accounts via HTTP

```bash
curl -H "Authorization: Bearer <admin-token>" \
     https://api.grok.com/api/admin/v1/accounts/12345

```

The response includes relationship metadata:

```json
{
  "id": 12345,
  "provider": "grok_web",
  "linkedAccountId": 67890,
  "linkedProvider": "grok_build",
  "linkedAccounts": [
    {
      "id": 67890,
      "provider": "grok_build",
      "name": "my-build-acct",
      "email": "build@example.com",
      "userId": "build-user"
    }
  ]
}

```

### Deleting with Cascading Peers

```json
POST /api/admin/v1/accounts/12345/delete
{
  "provider": "grok_web",
  "linkedDeleteTargets": ["grok_build"]
}

```

The handler processes the `linkedDeleteTargets` array to determine which associated providers should be removed alongside the primary account.

## Summary

- **Dual Table Architecture**: Account linking between Grok Web, Build, and Console relies on separate `account_provider_links` and `web_console_account_links` tables to maintain strict relationship cardinality
- **Transactional Safety**: All linking operations use PostgreSQL advisory locks and database transactions to prevent duplicate entries during concurrent access
- **API Integration**: The HTTP layer exposes linking capabilities through `/api/admin/v1/accounts/:id/link` with comprehensive linked account metadata in GET responses
- **Cascading Operations**: Deletion requests can propagate to linked accounts via `linkedDeleteTargets`, with the service layer handling ID resolution and atomic cleanup
- **Source Locations**: Core implementations reside in [`backend/internal/infra/persistence/relational/account_repository.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/infra/persistence/relational/account_repository.go) for data access and [`backend/internal/transport/http/account/handler.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/transport/http/account/handler.go) for HTTP transport concerns

## Frequently Asked Questions

### What database tables store the account linking relationships?

The system uses two tables defined in [`backend/internal/infra/persistence/relational/models.go`](https://github.com/chenyme/grok2api/blob/main/backend/internal/infra/persistence/relational/models.go): `account_provider_links` connects Grok Web to Grok Build accounts, while `web_console_account_links` connects Grok Web to Grok Console accounts. Both tables use composite primary keys and unique constraints to enforce one-to-one relationships between providers.

### How does grok2api prevent duplicate account links?

The implementation uses PostgreSQL advisory locks (`accountLinkLock`) to serialize concurrent linking attempts, combined with database-level unique constraints on the link tables. This ensures that two simultaneous requests cannot create conflicting associations between the same Web and Build (or Console) accounts.

### Can I link one Grok Build account to multiple Grok Web accounts?

No. The database schema enforces unique constraints that prevent many-to-many relationships. Each Build account can link to only one Web account, and each Web account can link to only one Build account. This one-to-one cardinality is maintained through the `chk_account_provider_links_distinct` check constraint.

### What happens to linked accounts when I delete a Grok Web account?

By default, only the Web account is deleted. However, if you specify `linkedDeleteTargets` in the deletion request (e.g., `["grok_build","grok_console"]`), the system resolves the linked IDs and performs an atomic deletion of all specified accounts. Additionally, foreign-key constraints with `ON DELETE CASCADE` automatically clean up orphaned entries in the link tables regardless of the cascading deletion option.