# Authentication Process Using QR Codes in wacli: How It Works and Why It Differs from Sync

> Learn how wacli QR code authentication creates new WhatsApp Web sessions and how it differs from the sync command. Understand the wacli auth process.

- Repository: [Peter Steinberger/wacli](https://github.com/steipete/wacli)
- Tags: internals
- Published: 2026-04-17

---

**The `wacli auth` command initiates an interactive QR code authentication flow that creates a new WhatsApp Web session, while the `wacli sync` command operates only on existing authenticated sessions and explicitly disables QR code prompts.**

`steipete/wacli` is a command-line interface for WhatsApp Web that distinguishes between initial authentication and subsequent synchronization. The repository implements a clear separation between the interactive QR code login flow and non-interactive sync operations through distinct command implementations and strict state validation.

## How QR Code Authentication Works in wacli

The authentication flow spans three architectural layers, from the CLI surface down to the low-level WhatsApp client implementation.

### The Three-Layer Architecture

| Layer | Responsibility | Key Source File |
|-------|----------------|-----------------|
| **CLI command** | Starts the process, configures QR options, renders the QR code in the terminal using **qrterminal**. | [`cmd/wacli/auth.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/auth.go) – the `AllowQR: true` flag and the `OnQRCode` callback |
| **App wrapper** | Opens the WhatsApp client and forwards QR options to the underlying connection. | [`internal/app/app.go`](https://github.com/steipete/wacli/blob/main/internal/app/app.go) – `Connect(ctx, allowQR, qrWriter)` forwards to `wa.Connect` |
| **WhatsApp client** | Connects to WhatsApp Web, obtains a QR channel when unauthenticated, streams events, and blocks until scan succeeds. | [`internal/wa/client.go`](https://github.com/steipete/wacli/blob/main/internal/wa/client.go) – `Connect` implements the QR flow handling `code`, `success`, `timeout`, and `error` events |

### Step-by-Step Authentication Flow

1. **User invokes `wacli auth`**  
   The command builds a `SyncOptions` struct with `AllowQR: true` and an `OnQRCode` callback that prints the code via `qrterminal.GenerateHalfBlock`.

2. **App layer forwards options**  
   `App.Sync` calls `a.Connect(ctx, true, qrWriter)`, which passes the boolean flag and writer to the low-level client.

3. **Client initializes QR channel**  
   Inside `wa.Connect`, the code checks `if !authed && connectOptions.AllowQR`. When true, it calls `cli.GetQRChannel(ctx)` to receive a channel of QR events.

4. **Event loop handles scan states**  
   After connecting to WhatsApp Web, the client enters a `select` loop reading from the QR channel:
   - **"code"**: Invokes the `OnQRCode` callback to render the terminal QR code.
   - **"success"**: Returns from the connect function, leaving the client authenticated.
   - **"timeout"** or **"error"**: Aborts with an error indicating authentication failure.

5. **Bootstrap proceeds**  
   Once authenticated, `App.Sync` continues with the regular bootstrap process (downloading messages, contacts, and groups).

## Key Differences Between `auth` and `sync` Commands

The `sync` command is explicitly designed as a **post-authentication** operation and enforces this constraint through code-level guards.

### Interactive vs. Non-Interactive Operation

- **`wacli auth`**: Interactive entry point. Sets `AllowQR: true` and provides the `OnQRCode` callback to enable visual QR code rendering in the terminal.
- **`wacli sync`**: Non-interactive, repeatable job. Sets `AllowQR: false` and passes `nil` for the QR writer, ensuring the process fails immediately if no session exists rather than prompting for a scan.

### Authentication Requirements

In [`cmd/wacli/sync.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/sync.go), the command first calls `a.EnsureAuthed()`, which fails fast if the store does not contain a valid session:

```go
if err := a.EnsureAuthed(); err != nil {
    return fmt.Errorf("not authenticated: %w", err)
}

```

The `SyncOptions` struct then explicitly disables QR:

```go
res, err := a.Sync(ctx, appPkg.SyncOptions{
    Mode:            mode,
    AllowQR:         false,  // QR disabled
    DownloadMedia:   downloadMedia,
    // ...
})

```

If the client is unauthenticated when `sync` runs, `wa.Connect` returns an error instructing the user to run `wacli auth` first.

## Implementation Details and Code Examples

### QR Code Rendering in auth.go

The [`cmd/wacli/auth.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/auth.go) file defines the interactive authentication command:

```go
res, err := a.Sync(ctx, appPkg.SyncOptions{
    Mode:            mode,
    AllowQR:         true,               // Enable QR flow
    DownloadMedia:   downloadMedia,
    RefreshContacts: true,
    RefreshGroups:   true,
    IdleExit:        idleExit,
    OnQRCode: func(code string) {          // Terminal QR renderer
        fmt.Fprintln(os.Stderr, "\nScan this QR code with WhatsApp (Linked Devices):")
        qrterminal.GenerateHalfBlock(code, qrterminal.M, os.Stderr)
        fmt.Fprintln(os.Stderr)
    },
})

```

### Non-Interactive Sync in sync.go

The [`cmd/wacli/sync.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/sync.go) file disables QR and requires existing authentication:

```go
// Ensure we have a stored session
if err := a.EnsureAuthed(); err != nil {
    return fmt.Errorf("not authenticated: %w", err)
}

// Run sync without QR support
res, err := a.Sync(ctx, appPkg.SyncOptions{
    Mode:            mode,
    AllowQR:         false,              // Explicitly disabled
    DownloadMedia:   downloadMedia,
    RefreshContacts: refreshContacts,
    RefreshGroups:   refreshGroups,
    IdleExit:        idleExit,
    MaxReconnect:    maxReconnect,
})

```

### Low-Level Client Logic in client.go

The [`internal/wa/client.go`](https://github.com/steipete/wacli/blob/main/internal/wa/client.go) file implements the event-driven QR flow:

```go
func (c *Client) Connect(ctx context.Context, allowQR bool, qrWriter func(string)) error {
    // Check if already authenticated
    authed := c.Store.HasSession()
    
    var qrChan <-chan whatsmeow.QRChannelItem
    if !authed && allowQR {
        // Request QR channel only when unauthenticated and allowed
        qrChan, _ = c.cli.GetQRChannel(ctx)
    }
    
    // Connect to WhatsApp Web
    if err := c.cli.ConnectContext(ctx); err != nil {
        return err
    }
    
    // If we need QR authentication, wait for scan
    if qrChan != nil {
        for evt := range qrChan {
            switch evt.Event {
            case "code":
                // Render the QR code
                qrWriter(evt.Code)
            case "success":
                // Authentication complete
                return nil
            case "timeout":
                return fmt.Errorf("QR code expired")
            case "error":
                return fmt.Errorf("QR error: %v", evt.Error)
            }
        }
    }
    
    return nil
}

```

## Summary

- **Interactive Authentication**: The `wacli auth` command enables QR code login by setting `AllowQR: true` and providing an `OnQRCode` callback that renders the code in the terminal using `qrterminal`.
- **Three-Layer Flow**: Authentication spans the CLI ([`cmd/wacli/auth.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/auth.go)), the app wrapper ([`internal/app/app.go`](https://github.com/steipete/wacli/blob/main/internal/app/app.go)), and the WhatsApp client ([`internal/wa/client.go`](https://github.com/steipete/wacli/blob/main/internal/wa/client.go)).
- **Event-Driven Protocol**: The client uses `GetQRChannel` to receive events (`code`, `success`, `timeout`, `error`) and blocks until the user scans the QR code with their mobile device.
- **Strict Separation**: The `wacli sync` command disables QR (`AllowQR: false`) and calls `EnsureAuthed()` to enforce that authentication must precede synchronization, making it suitable for automated, non-interactive workflows.

## Frequently Asked Questions

### Can I use `wacli sync` without first running `wacli auth`?

No. The `sync` command explicitly checks for existing authentication via `EnsureAuthed()` and returns an error if no session exists in the store. You must run `wacli auth` first to establish the session via QR code.

### What happens if I don't scan the QR code within the timeout period?

If the QR code expires, the [`internal/wa/client.go`](https://github.com/steipete/wacli/blob/main/internal/wa/client.go) event loop receives a `"timeout"` event from the QR channel and returns an error, causing the `wacli auth` command to exit with a failure status. You must restart the command to generate a fresh QR code.

### Where does wacli store authentication credentials after successful QR login?

The credentials are stored in the local database managed by the `Store` interface within the WhatsApp client. The [`client.go`](https://github.com/steipete/wacli/blob/main/client.go) implementation checks `c.Store.HasSession()` to determine if authentication exists, and the [`sync.go`](https://github.com/steipete/wacli/blob/main/sync.go) command uses `EnsureAuthed()` to verify this state before proceeding.

### Is the QR code authentication flow secure?

Yes, the implementation follows the official WhatsApp Web protocol. The QR code contains a temporary key exchange token that is only valid for a limited time (handled by the `"timeout"` event). The terminal rendering via `qrterminal` displays the code locally without transmitting it over networks, and the underlying `whatsmeow` library handles the cryptographic handshake once the mobile device scans the code.