# How the Local Contact Alias and Tagging System Works in wacli

> Discover how wacli's local contact alias and tagging system uses SQLite tables for nicknames and labels. Learn about CRUD operations and CLI commands to manage your contacts efficiently.

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

---

**The local contact alias and tagging system in wacli uses dedicated SQLite tables (`contact_aliases` and `contact_tags`) to store user-defined nicknames and labels, exposing full CRUD functionality through methods in [`internal/store/chats_contacts_groups.go`](https://github.com/steipete/wacli/blob/main/internal/store/chats_contacts_groups.go) and CLI commands in [`cmd/wacli/contacts.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/contacts.go).**

wacli is a WhatsApp CLI client that maintains a local SQLite cache of contact information. To augment the basic contact data provided by WhatsApp, wacli implements a flexible **local contact alias and tagging system** that allows users to assign custom nicknames and categorical labels without modifying the underlying WhatsApp data. This system operates entirely within the local database, ensuring privacy and immediate availability for search and display operations.

## Database Schema for Aliases and Tags

The schema is defined in [`internal/store/migrations.go`](https://github.com/steipete/wacli/blob/main/internal/store/migrations.go) and creates two independent tables that reference contacts by their JID (Jabber ID).

### contact_aliases Table

The `contact_aliases` table stores one-to-one mappings between a contact's JID and a user-defined alias:

```sql
-- internal/store/migrations.go
CREATE TABLE IF NOT EXISTS contact_aliases (
    jid TEXT PRIMARY KEY,
    alias TEXT NOT NULL,
    notes TEXT,
    updated_at INTEGER NOT NULL
);

```

### contact_tags Table

The `contact_tags` table supports many-to-many relationships, allowing multiple tags per contact:

```sql
-- internal/store/migrations.go
CREATE TABLE IF NOT EXISTS contact_tags (
    jid TEXT NOT NULL,
    tag TEXT NOT NULL,
    updated_at INTEGER NOT NULL,
    PRIMARY KEY (jid, tag)
);

```

## Core CRUD Operations

All database operations are implemented in [`internal/store/chats_contacts_groups.go`](https://github.com/steipete/wacli/blob/main/internal/store/chats_contacts_groups.go) using upsert-style SQL statements.

### Managing Contact Aliases

**SetAlias** creates or updates an alias for a specific JID:

```go
// internal/store/chats_contacts_groups.go (lines 43-55)
func (d *DB) SetAlias(jid, alias string) error {
    // Implementation trims whitespace and validates non-empty input
    // Uses INSERT ... ON CONFLICT (jid) DO UPDATE ...
}

```

**RemoveAlias** deletes the alias record:

```go
// internal/store/chats_contacts_groups.go (lines 57-60)
func (d *DB) RemoveAlias(jid string) error {
    // DELETE FROM contact_aliases WHERE jid = ?
}

```

### Managing Contact Tags

**AddTag** attaches a label to a contact:

```go
// internal/store/chats_contacts_groups.go (lines 62-73)
func (d *DB) AddTag(jid, tag string) error {
    // Normalizes tag format, validates, then upserts into contact_tags
}

```

**RemoveTag** detaches a specific label:

```go
// internal/store/chats_contacts_groups.go (lines 75-78)
func (d *DB) RemoveTag(jid, tag string) error {
    // DELETE FROM contact_tags WHERE jid = ? AND tag = ?
}

```

**ListTags** retrieves all tags for a contact in alphabetical order:

```go
// internal/store/chats_contacts_groups.go (lines 28-44)
func (d *DB) ListTags(jid string) ([]string, error) {
    // SELECT tag FROM contact_tags WHERE jid = ? ORDER BY tag ASC
}

```

### Retrieving Enriched Contact Data

The **GetContact** method hydrates the full `Contact` struct by joining with the alias table and invoking `ListTags`:

```go
// internal/store/chats_contacts_groups.go (lines 6-26)
func (d *DB) GetContact(jid string) (Contact, error) {
    // JOINs contacts with contact_aliases
    // Calls ListTags to populate the Tags slice
}

```

The `Contact` struct definition in [`internal/store/types.go`](https://github.com/steipete/wacli/blob/main/internal/store/types.go) includes these fields:

```go
type Contact struct {
    JID       string
    Phone     string
    Name      string
    Alias     string   // From contact_aliases
    Tags      []string // From contact_tags
    UpdatedAt time.Time
}

```

## Integration with Contact Search

The **SearchContacts** function in [`internal/store/chats_contacts_groups.go`](https://github.com/steipete/wacli/blob/main/internal/store/chats_contacts_groups.go) treats aliases as first-class search fields. The SQL query checks the alias column alongside native contact fields:

```go
WHERE LOWER(COALESCE(a.alias,'')) LIKE LOWER(?)
   OR LOWER(COALESCE(c.full_name,''))   LIKE LOWER(?)
   OR LOWER(COALESCE(c.push_name,''))   LIKE LOWER(?)
   OR LOWER(COALESCE(c.phone,''))       LIKE LOWER(?)
   OR LOWER(c.jid)                      LIKE LOWER(?)

```

The `ORDER BY` clause prioritizes alias matches, ensuring that user-defined nicknames appear before auto-fetched WhatsApp names in search results.

## CLI Interface for Alias and Tag Management

The command-line interface is implemented in [`cmd/wacli/contacts.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/contacts.go). Each sub-command wraps the corresponding database method:

```bash

# Set an alias

wacli contacts alias set --jid 1234567890@s.whatsapp.net --alias "Mom"

# Remove an alias

wacli contacts alias rm --jid 1234567890@s.whatsapp.net

# Add a tag

wacli contacts tags add --jid 1234567890@s.whatsapp.net --tag family

# Remove a tag

wacli contacts tags rm --jid 1234567890@s.whatsapp.net --tag family

```

Success returns "OK", while the `--json` flag enables JSON output for integration with external scripts.

## Programmatic Usage Examples

### Setting an Alias in Go

```go
// Example: Assigning a nickname to a contact
if err := db.SetAlias("1234567890@s.whatsapp.net", "Mom"); err != nil {
    log.Fatalf("failed to set alias: %v", err)
}

```

*Source:* `SetAlias` implementation in [`internal/store/chats_contacts_groups.go`](https://github.com/steipete/wacli/blob/main/internal/store/chats_contacts_groups.go) (lines 43-55).

### Adding a Tag

```go
// Example: Categorizing a contact
if err := db.AddTag("1234567890@s.whatsapp.net", "family"); err != nil {
    log.Fatalf("failed to add tag: %v", err)
}

```

*Source:* `AddTag` in [`internal/store/chats_contacts_groups.go`](https://github.com/steipete/wacli/blob/main/internal/store/chats_contacts_groups.go) (lines 62-73).

### Retrieving Full Contact Data

```go
// Example: Loading contact with enriched metadata
contact, err := db.GetContact("1234567890@s.whatsapp.net")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Name: %s, Alias: %s, Tags: %v\n", 
    contact.Name, contact.Alias, contact.Tags)

```

*Source:* `GetContact` in [`internal/store/chats_contacts_groups.go`](https://github.com/steipete/wacli/blob/main/internal/store/chats_contacts_groups.go) (lines 6-26).

## Summary

- **wacli** implements a **local contact alias and tagging system** using SQLite tables `contact_aliases` and `contact_tags` that operate independently of WhatsApp's native contact data.
- **Aliases** provide one-to-one nicknames for JIDs, while **tags** offer many-to-many categorical labeling.
- **CRUD operations** (`SetAlias`, `RemoveAlias`, `AddTag`, `RemoveTag`, `ListTags`) reside in [`internal/store/chats_contacts_groups.go`](https://github.com/steipete/wacli/blob/main/internal/store/chats_contacts_groups.go) and use upsert-style SQL for atomic updates.
- **Data hydration** occurs in `GetContact`, which joins the aliases table and populates the `Tags` slice via `ListTags`, ensuring the `Contact` struct always contains enriched metadata.
- **Search integration** treats aliases as primary fields, prioritizing user-defined nicknames over native names in result ordering.
- **CLI exposure** in [`cmd/wacli/contacts.go`](https://github.com/steipete/wacli/blob/main/cmd/wacli/contacts.go) provides thin wrappers for shell scripting and JSON output.

## Frequently Asked Questions

### How does wacli store contact aliases and tags?

wacli stores aliases and tags in separate SQLite tables named `contact_aliases` and `contact_tags` within the local database. The `contact_aliases` table uses the JID as a primary key for one-to-one nickname storage, while `contact_tags` uses a composite primary key of `(jid, tag)` to support multiple labels per contact. This schema is defined in [`internal/store/migrations.go`](https://github.com/steipete/wacli/blob/main/internal/store/migrations.go) and allows the system to enrich WhatsApp contact data without modifying the native WhatsApp fields.

### Can I search contacts by their alias or tags?

Yes, the `SearchContacts` function in [`internal/store/chats_contacts_groups.go`](https://github.com/steipete/wacli/blob/main/internal/store/chats_contacts_groups.go) explicitly includes the alias column in its search predicate. The SQL query checks `WHERE LOWER(COALESCE(a.alias,'')) LIKE LOWER(?)` alongside native fields like `full_name` and `push_name`. Furthermore, the `ORDER BY` clause prioritizes alias matches, ensuring that contacts with user-defined nicknames appear at the top of search results. Tags can be filtered programmatically by retrieving contacts and checking their `Tags` slice.

### What happens if I set an alias for a contact that already has one?

The `SetAlias` method implements an upsert pattern using SQLite's `INSERT ... ON CONFLICT (jid) DO UPDATE` syntax. When you call `SetAlias` with a JID that already exists in the `contact_aliases` table, the existing alias is replaced with the new value and the `updated_at` timestamp is refreshed. This ensures that setting an alias is always an atomic operation that either creates a new record or updates an existing one without requiring explicit deletion first.

### Are contact aliases and tags available when exporting or listing contacts?

Yes, because the `Contact` struct in [`internal/store/types.go`](https://github.com/steipete/wacli/blob/main/internal/store/types.go) includes both `Alias` (string) and `Tags` ([]string) fields, any operation that retrieves contacts—including `GetContact` and `SearchContacts`—automatically hydrates these fields. When using CLI commands like `wacli contacts list` with the `--json` flag, the exported JSON objects include the `alias` and `tags` properties. This ensures that user-defined metadata persists across exports, backups, and integration scripts.