Skip to main content

Organization

Teak provides multiple ways to organize and filter your cards: favorites for quick access, tags for categorization, and advanced filters for precise queries.

Favorites

Mark important cards as favorites for quick access and filtering.

Favoriting Cards

Cards have an isFavorited boolean flag:
// Mark as favorite
await ctx.db.patch(cardId, {
  isFavorited: true
});

// Remove from favorites
await ctx.db.patch(cardId, {
  isFavorited: false
});

Querying Favorites

Favorites are indexed for efficient queries:
// Get all favorites
const favorites = await ctx.db
  .query("cards")
  .withIndex("by_user_favorites_deleted", (q) =>
    q.eq("userId", userId)
     .eq("isFavorited", true)
     .eq("isDeleted", undefined)
  )
  .order("desc")
  .collect();
Index definition:
.index("by_user_favorites", ["userId", "isFavorited"])
.index("by_user_favorites_deleted", ["userId", "isFavorited", "isDeleted"])

Search Integration

All search indexes support favorites filtering:
// Search favorites only
const results = await searchCards({
  searchQuery: "design patterns",
  favoritesOnly: true,
  limit: 50
});
Search indexes include isFavorited as a filter field:
.searchIndex("search_content", {
  searchField: "content",
  filterFields: ["userId", "isDeleted", "type", "isFavorited"],
})

Special Keywords

Quick access via search:
// Any of these keywords returns favorites
"fav"
"favs" 
"favorites"
"favourite"
"favourites"
Implementation:
if (["fav", "favs", "favorites", "favourite", "favourites"].includes(query)) {
  const favorites = await ctx.db
    .query("cards")
    .withIndex("by_user_favorites_deleted", (q) =>
      q.eq("userId", userId)
       .eq("isFavorited", true)
       .eq("isDeleted", undefined)
    )
    .order("desc")
    .take(limit);
  return favorites;
}
Favorites are user-specific. You can favorite unlimited cards without affecting storage limits.

Tags

Tags provide flexible categorization with both user-defined and AI-generated options.

User Tags

Manually add tags to any card:
// Add tags
await ctx.db.patch(cardId, {
  tags: ["react", "tutorial", "hooks", "javascript"]
});

// Update tags (merge new tags)
const card = await ctx.db.get(cardId);
const existingTags = card.tags || [];
const newTags = ["typescript", "frontend"];
await ctx.db.patch(cardId, {
  tags: [...new Set([...existingTags, ...newTags])]
});

// Remove a tag
const card = await ctx.db.get(cardId);
await ctx.db.patch(cardId, {
  tags: card.tags?.filter(t => t !== "tutorial")
});

AI Tags

Automatic topic extraction during AI processing:
// AI tags are generated automatically
aiTags: ["web development", "frontend", "component architecture", "react"]
AI tags are:
  • Read-only: Generated by AI, not user-editable
  • Searchable: Indexed for full-text search
  • Complementary: Work alongside user tags

Tag Schema

Both user and AI tags are stored as string arrays:
{
  tags: v.optional(v.array(v.string())),      // User-defined
  aiTags: v.optional(v.array(v.string())),    // AI-generated
}
Tags have dedicated search indexes:
.searchIndex("search_tags", {
  searchField: "tags",
  filterFields: ["userId", "isDeleted", "type", "isFavorited"],
})
.searchIndex("search_ai_tags", {
  searchField: "aiTags",
  filterFields: ["userId", "isDeleted", "type", "isFavorited"],
})

Partial Tag Matching

Tag search supports partial matching:
// Search for "java" matches:
// - "javascript"
// - "java"
// - "Java Programming"

const searchTerms = "java".toLowerCase().split(/\s+/);
const matches = allCards.filter((card) =>
  card.tags?.some((tag) =>
    searchTerms.some((term) => tag.toLowerCase().includes(term))
  )
);

Tag Best Practices

Choose a consistent format:
  • lowercase: javascript, react, tutorial
  • kebab-case: web-development, ui-design
  • camelCase: webDevelopment, uiDesign
Stick to one convention for easier filtering.
Good tags:
  • react, hooks, tutorial, beginner
Avoid:
  • react tutorial for beginners using hooks (too specific)
  • Use multiple focused tags instead
  • User tags: Project-specific, workflow categories
  • AI tags: Automatic topic detection, content classification
  • Together: Comprehensive organization
Use prefixes for hierarchical organization:
  • project:teak, project:blog
  • topic:design, topic:development
  • status:todo, status:done

Filters

Advanced filtering options for precise card queries.

Type Filters

Filter by one or more card types:
// Single type
const images = await searchCards({
  types: ["image"],
  limit: 50
});

// Multiple types
const media = await searchCards({
  types: ["image", "video", "audio"],
  limit: 50
});
Implementation uses indexes:
// Single type: uses index
if (types && types.length === 1) {
  query = ctx.db.query("cards")
    .withIndex("by_user_type_deleted", (q) =>
      q.eq("userId", userId)
       .eq("type", types[0])
       .eq("isDeleted", undefined)
    );
}

// Multiple types: uses OR filter
else if (types && types.length > 1) {
  query = query.filter((q) => {
    const typeConditions = types.map((type) => q.eq(q.field("type"), type));
    return typeConditions.reduce((acc, condition) => q.or(acc, condition));
  });
}
Single type filters use indexes for optimal performance. Multiple type filters require post-index OR filtering.

Date Range Filters

Filter by creation date:
// Last 7 days
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
const recent = await searchCards({
  createdAtRange: {
    start: sevenDaysAgo,
    end: Date.now()
  },
  limit: 50
});

// Specific date range
const startOfMonth = new Date(2024, 0, 1).getTime();
const endOfMonth = new Date(2024, 0, 31).getTime();
const january = await searchCards({
  createdAtRange: {
    start: startOfMonth,
    end: endOfMonth
  },
  limit: 50
});
Uses dedicated date index:
.index("by_created", ["userId", "createdAt"])

query = ctx.db
  .query("cards")
  .withIndex("by_created", (q) =>
    q.eq("userId", userId)
     .gte("createdAt", range.start)
     .lt("createdAt", range.end)
  );

Visual Style Filters

Filter images by aesthetic style:
const minimal = await searchCards({
  types: ["image"],
  styleFilters: ["minimal", "monochrome"],
  limit: 50
});

Available Visual Styles

const VISUAL_STYLE_TAXONOMY = [
  "abstract",      // Abstract art, patterns
  "cinematic",     // Film-like, dramatic shots
  "dark",          // Low-key lighting, dark tones
  "illustrative",  // Drawings, cartoons, illustrations
  "minimal",       // Simple, clean, negative space
  "monochrome",    // Black and white, grayscale
  "moody",         // Atmospheric, dramatic lighting
  "pastel",        // Soft, muted colors
  "photographic",  // Realistic photography
  "retro",         // Vintage aesthetic, synthwave
  "surreal",       // Dreamlike, unusual
  "vintage",       // Old, aged appearance
  "vibrant",       // Bright, saturated colors
] as const;

Style Normalization

Aliases are automatically mapped to canonical styles:
// "minimalist" → "minimal"
// "b&w" → "monochrome" 
// "colorful" → "vibrant"
// "photo" → "photographic"

const normalized = normalizeVisualStyle("colorful");
// Returns: "vibrant"

Color Hue Filters

Filter by color palette hues:
const blueImages = await searchCards({
  types: ["image", "palette"],
  hueFilters: ["blue", "cyan"],
  limit: 50
});

Available Hue Buckets

const COLOR_HUE_BUCKETS = [
  "red",
  "orange",
  "yellow",
  "green",
  "teal",
  "cyan",
  "blue",
  "purple",
  "pink",
  "brown",
  "neutral",  // Grays, blacks, whites
] as const;

Hue Normalization

Aliases map to canonical hues:
// "violet" → "purple"
// "indigo" → "purple"
// "magenta" → "pink"
// "gray" → "neutral"
// "monochrome" → "neutral"

const normalized = normalizeColorHueBucket("magenta");
// Returns: "pink"

Hex Color Filters

Find exact color matches:
const teakColors = await searchCards({
  types: ["image", "palette"],
  hexFilters: ["#8B7355", "#D4A574"],
  limit: 50
});
Searches the colorHexes field:
colorHexes: ["#8B7355", "#D4A574", "#5C4A3A", ...]

Trash Filters

View deleted cards:
// Show trash only
const trash = await searchCards({
  showTrashOnly: true,
  limit: 50
});
Special keywords also work:
"trash"
"deleted"
"bin"
"recycle"
"trashed"
Implementation:
if (["trash", "deleted", "bin", "recycle", "trashed"].includes(query)) {
  const trashed = await ctx.db
    .query("cards")
    .withIndex("by_user_deleted", (q) =>
      q.eq("userId", userId).eq("isDeleted", true)
    )
    .order("desc")
    .take(limit);
  return trashed;
}

Combined Filters

All filters can be combined:
// Complex query: Recent favorite minimal blue images
const results = await searchCards({
  types: ["image"],
  favoritesOnly: true,
  styleFilters: ["minimal"],
  hueFilters: ["blue"],
  createdAtRange: {
    start: Date.now() - (30 * 24 * 60 * 60 * 1000),
    end: Date.now()
  },
  limit: 50
});

Filter Performance

Index-backed (fast):
  • User ID scoping
  • Deletion status
  • Single type filter
  • Favorites filter
  • Date range filter
In-memory (slower for large sets):
  • Multiple type filters (OR condition)
  • Visual style filters
  • Color hue/hex filters
  • Combined visual filters
Visual filters (styles, hues, hexes) require in-memory filtering. For best performance, combine with type or date filters to reduce the result set first.

Filter Helpers

Reusable filter logic:

Visual Filter Normalization

import {
  normalizeVisualFilterArgs,
  applyCardLevelFilters,
  doesCardMatchVisualFilters
} from "./visualFilters";

const visualFilters = normalizeVisualFilterArgs({
  styleFilters: ["minimal", "vibrant"],
  hueFilters: ["blue", "cyan"],
  hexFilters: ["#8B7355"]
});

// Returns:
{
  hasVisualFilters: true,
  styles: ["minimal", "vibrant"],
  hues: ["blue", "cyan"],
  hexes: ["#8B7355"]
}

Card-Level Filtering

// Apply all filters to a card array
const filtered = applyCardLevelFilters(cards, {
  types: ["image"],
  favoritesOnly: true,
  createdAtRange: { start: timestamp, end: now },
  visualFilters
});

Visual Filter Matching

// Check if card matches visual filters
const matches = doesCardMatchVisualFilters(card, visualFilters);

Organization Examples

// Tag cards by project
await ctx.db.patch(cardId, {
  tags: ["project:teak", "docs", "reference"]
});

// Find all project cards
const projectCards = await searchCards({
  searchQuery: "project:teak",
  limit: 100
});

Next Steps

Search

Master full-text search across all fields

Card Types

Understand type-specific organization

Organizing Guide

Best practices and workflows

Color Palettes

Deep dive into color filtering

Build docs developers (and LLMs) love