Skip to main content

Overview

The Store Queries API provides functions for retrieving store information, primarily for authenticated store owners to access their own store data. This is the primary way stores get their profile information for dashboard and management interfaces. Location: convex/stores/queries.ts

Get Store by Authenticated Owner

Retrieves complete store information for the authenticated store owner.
const store = await convex.query(api.stores.queries.getStoreByAuthenticatedOwner);
{
  "_id": "j123456789",
  "name": "Mario's Pizza Palace",
  "primaryCategory": "k123456789",
  "status": "approved",
  "statusReason": "All documents verified and approved",
  "logoUrl": "https://storage.convex.dev/logo123.jpg",
  "coverImageUrl": "https://storage.convex.dev/cover456.jpg",
  "tradeLicenseUrl": "https://storage.convex.dev/license789.pdf",
  "ownerUserId": "user_123456789",
  "ownerName": "Mario Rossi",
  "ownerEmail": "[email protected]",
  "settings": {
    "deliveryEnabled": true,
    "pickupEnabled": true,
    "storeLive": true
  },
  "bankAccount": {
    "IBAN": "AE123456789012345678901",
    "bankName": "Emirates NBD",
    "bankId": "b123456789",
    "accountHolderName": "Mario Rossi"
  },
  "category": {
    "id": "k123456789",
    "name": "Food & Restaurants"
  },
  "address": {
    "fullAddress": "123 Main Street, Business Bay, Dubai, UAE",
    "city": {
      "_id": "c123456789",
      "name": "Dubai",
      "nameArabic": "دبي"
    },
    "area": {
      "_id": "a987654321",
      "name": "Business Bay",
      "city": "c123456789"
    },
    "flatVilaNumber": "Villa 123",
    "buildingNameNumber": "Marina Plaza",
    "landmark": "Near Dubai Mall",
    "latitude": "25.1972",
    "longitude": "55.2744"
  },
  "wallet": {
    "availableBalance": 1250.50,
    "pendingBalance": 340.25,
    "totalBalance": 1590.75
  },
  "stats": {
    "totalProducts": 25,
    "activeProducts": 23,
    "totalOrders": 150,
    "averageRating": 4.8
  },
  "_creationTime": 1640995200000,
  "lastUpdated": 1640995800000
}

Store Dashboard Hook

Complete React hook for store dashboard data:
import { useQuery } from 'convex/react';
import { api } from './convex/_generated/api';

interface StoreData {
  store: any;
  isLoading: boolean;
  error: string | null;
  isApproved: boolean;
  isPending: boolean;
  isRejected: boolean;
}

function useStoreData(): StoreData {
  const store = useQuery(api.stores.queries.getStoreByAuthenticatedOwner);
  
  const isLoading = store === undefined;
  const error = store === null ? "No store found for this user" : null;
  
  const isApproved = store?.status === "approved";
  const isPending = store?.status === "pending";
  const isRejected = store?.status === "rejected";

  return {
    store,
    isLoading,
    error,
    isApproved,
    isPending,
    isRejected
  };
}

// Usage in component
function StoreDashboard() {
  const { store, isLoading, error, isApproved, isPending, isRejected } = useStoreData();

  if (isLoading) {
    return <div className="flex items-center justify-center p-8">
      <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
      <span className="ml-2">Loading store...</span>
    </div>;
  }

  if (error) {
    return <div className="bg-red-50 p-4 rounded-lg">
      <p className="text-red-700">{error}</p>
      <button className="mt-2 bg-blue-600 text-white px-4 py-2 rounded">
        Register Store
      </button>
    </div>;
  }

  if (isPending) {
    return <div className="bg-yellow-50 p-6 rounded-lg">
      <h2 className="text-xl font-bold text-yellow-900 mb-2">Application Under Review</h2>
      <p className="text-yellow-700 mb-4">
        Your store application is being reviewed by our team. This typically takes 24-48 hours.
      </p>
      <div className="bg-white p-4 rounded border">
        <h3 className="font-medium mb-2">Submitted Information:</h3>
        <ul className="text-sm text-gray-600 space-y-1">
          <li>• Store Name: {store.name}</li>
          <li>• Category: {store.category?.name}</li>
          <li>• Trade License: Uploaded</li>
          <li>• Submitted: {new Date(store._creationTime).toLocaleDateString()}</li>
        </ul>
      </div>
    </div>;
  }

  if (isRejected) {
    return <div className="bg-red-50 p-6 rounded-lg">
      <h2 className="text-xl font-bold text-red-900 mb-2">Application Rejected</h2>
      <p className="text-red-700 mb-4">{store.statusReason}</p>
      <button className="bg-blue-600 text-white px-4 py-2 rounded">
        Update Application
      </button>
    </div>;
  }

  // Approved store dashboard
  return (
    <div className="space-y-6">
      {/* Store Header */}
      <div className="bg-white rounded-lg shadow p-6">
        <div className="flex items-center space-x-4">
          {store.logoUrl && (
            <img 
              src={store.logoUrl} 
              alt={store.name}
              className="w-16 h-16 rounded-lg object-cover"
            />
          )}
          <div>
            <h1 className="text-2xl font-bold text-gray-900">{store.name}</h1>
            <p className="text-gray-600">{store.category?.name}</p>
            <div className="flex items-center space-x-4 mt-2">
              <span className={`px-2 py-1 rounded-full text-xs font-medium ${
                store.settings?.storeLive 
                  ? 'bg-green-100 text-green-800' 
                  : 'bg-gray-100 text-gray-800'
              }`}>
                {store.settings?.storeLive ? 'Live' : 'Offline'}
              </span>
              <span className="text-sm text-gray-500">
                ⭐ {store.stats?.averageRating || 0} rating
              </span>
            </div>
          </div>
        </div>
      </div>

      {/* Quick Stats */}
      <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
        <div className="bg-white p-4 rounded-lg shadow">
          <p className="text-sm text-gray-600">Total Products</p>
          <p className="text-2xl font-bold text-gray-900">{store.stats?.totalProducts || 0}</p>
        </div>
        <div className="bg-white p-4 rounded-lg shadow">
          <p className="text-sm text-gray-600">Active Products</p>
          <p className="text-2xl font-bold text-green-600">{store.stats?.activeProducts || 0}</p>
        </div>
        <div className="bg-white p-4 rounded-lg shadow">
          <p className="text-sm text-gray-600">Total Orders</p>
          <p className="text-2xl font-bold text-blue-600">{store.stats?.totalOrders || 0}</p>
        </div>
        <div className="bg-white p-4 rounded-lg shadow">
          <p className="text-sm text-gray-600">Average Rating</p>
          <p className="text-2xl font-bold text-yellow-600">
            {store.stats?.averageRating ? store.stats.averageRating.toFixed(1) : '0.0'}
          </p>
        </div>
      </div>

      {/* Store Settings */}
      <div className="bg-white rounded-lg shadow p-6">
        <h2 className="text-lg font-semibold mb-4">Store Settings</h2>
        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
          <div className="flex items-center justify-between p-3 bg-gray-50 rounded">
            <span>Delivery Service</span>
            <span className={`px-2 py-1 rounded text-xs font-medium ${
              store.settings?.deliveryEnabled 
                ? 'bg-green-100 text-green-800' 
                : 'bg-red-100 text-red-800'
            }`}>
              {store.settings?.deliveryEnabled ? 'Enabled' : 'Disabled'}
            </span>
          </div>
          <div className="flex items-center justify-between p-3 bg-gray-50 rounded">
            <span>Pickup Service</span>
            <span className={`px-2 py-1 rounded text-xs font-medium ${
              store.settings?.pickupEnabled 
                ? 'bg-green-100 text-green-800' 
                : 'bg-red-100 text-red-800'
            }`}>
              {store.settings?.pickupEnabled ? 'Enabled' : 'Disabled'}
            </span>
          </div>
          <div className="flex items-center justify-between p-3 bg-gray-50 rounded">
            <span>Store Status</span>
            <span className={`px-2 py-1 rounded text-xs font-medium ${
              store.settings?.storeLive 
                ? 'bg-green-100 text-green-800' 
                : 'bg-gray-100 text-gray-800'
            }`}>
              {store.settings?.storeLive ? 'Live' : 'Offline'}
            </span>
          </div>
        </div>
      </div>

      {/* Bank Account Info */}
      {store.bankAccount && (
        <div className="bg-white rounded-lg shadow p-6">
          <h2 className="text-lg font-semibold mb-4">Bank Account</h2>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
            <div>
              <p className="text-sm text-gray-600">Bank Name</p>
              <p className="font-medium">{store.bankAccount.bankName}</p>
            </div>
            <div>
              <p className="text-sm text-gray-600">Account Holder</p>
              <p className="font-medium">{store.bankAccount.accountHolderName}</p>
            </div>
            <div className="md:col-span-2">
              <p className="text-sm text-gray-600">IBAN</p>
              <p className="font-mono text-sm bg-gray-50 p-2 rounded">
                {store.bankAccount.IBAN}
              </p>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

export { useStoreData, StoreDashboard };

Store Status Indicators

Visual indicators for different store states:

Pending Review

Store application submitted and waiting for admin review

Approved

Store is approved and can start selling products

Rejected

Store application rejected with reason provided

Live

Store is live and accepting orders from customers

Store Information Structure

The store query returns comprehensive information:
interface StoreData {
  // Basic Information
  _id: string;
  name: string;
  primaryCategory: string;
  status: 'pending' | 'approved' | 'rejected';
  statusReason?: string;
  
  // Media Assets
  logoUrl?: string;
  coverImageUrl?: string;
  tradeLicenseUrl: string;
  
  // Owner Information
  ownerUserId: string;
  ownerName: string;
  ownerEmail: string;
  
  // Operational Settings
  settings: {
    deliveryEnabled: boolean;
    pickupEnabled: boolean;
    storeLive: boolean;
  };
  
  // Financial Information
  bankAccount?: {
    IBAN: string;
    bankName: string;
    bankId: string;
    accountHolderName: string;
  };

  // Address Information (enriched with city/area details)
  address?: {
    fullAddress?: string;
    city?: {
      _id: string;
      name: string;
      nameArabic: string;
    };
    area?: {
      _id: string;
      name: string;
      city: string;
    };
    flatVilaNumber?: string;
    buildingNameNumber?: string;
    landmark?: string;
    latitude?: string;
    longitude?: string;
  };

  // Wallet Information
  wallet: {
    availableBalance: number;
    pendingBalance: number;
    totalBalance: number;
  };

  // Category Information
  category: {
    id: string;
    name: string;
  };
  
  // Statistics
  stats: {
    totalProducts: number;
    activeProducts: number;
    totalOrders: number;
    averageRating: number;
  };
  
  // Timestamps
  _creationTime: number;
  lastUpdated: number;
}

Get Store Address

Retrieve only the address information for a store with enriched city and area details.
storeId
Id<'stores'>
Optional store ID. If not provided, returns address for authenticated owner’s store.
// Get address for authenticated store owner
const storeAddress = await convex.query(api.stores.queries.getStoreAddress);

// Get address for specific store (must be owned by authenticated user)
const specificStoreAddress = await convex.query(api.stores.queries.getStoreAddress, {
  storeId: "j123456789"
});
{
  "_id": "j123456789",
  "address": {
    "fullAddress": "123 Main Street, Business Bay, Dubai, UAE",
    "city": {
      "_id": "c123456789",
      "name": "Dubai",
      "nameArabic": "دبي"
    },
    "area": {
      "_id": "a987654321",
      "name": "Business Bay",
      "city": "c123456789"
    },
    "flatVilaNumber": "Villa 123",
    "buildingNameNumber": "Marina Plaza",
    "landmark": "Near Dubai Mall",
    "latitude": "25.1972",
    "longitude": "55.2744"
  }
}
Returns null if the authenticated user doesn’t have a registered store and no storeId is provided.

Get Store Orders

Retrieve orders for the authenticated store with filtering and pagination support.
status
OrderStatus
Filter orders by status: “pending”, “confirmed”, “preparing”, “ready”, “out_for_delivery”, “delivered”, “cancelled”
limit
number
Maximum number of orders to return (default: 50)
// Get all orders
const allOrders = await convex.query(api.stores.queries.getStoreOrders);

// Get only pending orders
const pendingOrders = await convex.query(api.stores.queries.getStoreOrders, {
  status: "pending"
});

// Get confirmed orders with limit
const confirmedOrders = await convex.query(api.stores.queries.getStoreOrders, {
  status: "confirmed",
  limit: 20
});
[
  {
    "_id": "o123456789",
    "_creationTime": 1640995200000,
    "customerId": "cu123456789",
    "customerName": "Ahmed Al-Mansouri",
    "customerPhone": "+971501234567",
    "deliveryAddress": {
      "fullAddress": "Building 23, Street 45, Al Karama, Dubai",
      "city": "Dubai",
      "area": "Al Karama",
      "flatVilaNumber": "Apt 304",
      "buildingNameNumber": "Golden Tower",
      "phoneNumber": "+971501234567",
      "landmark": "Near Karama Park"
    },
    "subtotal": 125.50,
    "deliveryFee": 15.00,
    "totalAmount": 140.50,
    "storeEarnings": 112.40,
    "status": "pending",
    "paymentStatus": "paid",
    "orderDate": 1640995200000,
    "itemsCount": 3,
    "deliveryType": "delivery",
    "orderItems": [
      {
        "productId": "p123456789",
        "productName": "Margherita Pizza",
        "productImageUrl": "https://storage.convex.dev/pizza123.jpg",
        "quantity": 2,
        "unitPrice": 45.00,
        "totalPrice": 90.00
      },
      {
        "productId": "p987654321",
        "productName": "Caesar Salad",
        "productImageUrl": "https://storage.convex.dev/salad456.jpg",
        "quantity": 1,
        "unitPrice": 35.50,
        "totalPrice": 35.50
      }
    ]
  }
]

Real-time Updates

The store query automatically updates when store information changes:
// Store data updates automatically
function StoreProfile() {
  const store = useQuery(api.stores.queries.getStoreByAuthenticatedOwner);
  
  // This will re-render when:
  // - Store status changes (approved/rejected)
  // - Store settings are updated
  // - Bank account information is added/updated
  // - Store statistics change
  
  return (
    <div>
      {store && (
        <div>
          <h1>{store.name}</h1>
          <p>Status: {store.status}</p>
          <p>Live: {store.settings.storeLive ? 'Yes' : 'No'}</p>
        </div>
      )}
    </div>
  );
}

Error Handling

Response: nullThis indicates the authenticated user doesn’t have a registered store yet.
if (store === null) {
  // User needs to register a store
  return <RegisterStoreComponent />;
}
Status Code: 401
{
  "error": "Authentication required to access store data"
}
Status Code: 403
{
  "error": "Access denied. User is not the owner of this store."
}

Best Practices

Real-time Updates

Use the React hook pattern to automatically receive updates when store data changes

Status Handling

Always check store status before showing management features

Loading States

Handle loading states gracefully while store data is being fetched

Error Boundaries

Implement proper error handling for missing or inaccessible stores
This query only returns data for the authenticated user’s own store. Store owners cannot access other stores’ private information.
Use the store status to conditionally show different UI components - registration forms for new users, pending status for under review, and full dashboard for approved stores.