Site icon Multidots

Behind the Build: Creating a Harvest-like Time Tracking Platform on Sanity

Modern teams need more than a time tracker. We need a workforce operating system — one that unifies time tracking, project management, team oversight, client billing, and performance reporting into a single workflow.

We developed a full-featured time tracking platform inside Sanity that mirrors the core experience of Harvest — and we built the Multitime tool in less than 3 months.

Our goal was simple: combine the power of professional time tracking with the flexibility of a fully programmable content platform.

This blog breaks down how we built a full Harvest-style time tracking + team management platform inside Sanity, using Next.js and Google OAuth, in less than 3 months.

Why We Looked at Harvest and What Inspired Us

Before building our current system, our team used Harvest for time tracking across multiple client projects. This gave us valuable hands-on experience with what makes a strong time tracking product.

Here are the key features that impressed us:

That experience shaped our thinking and became a key reference point as we set out to build our own solution — one that keeps the best of the Harvest model while offering greater flexibility, control, and deep integration with our Sanity-powered ecosystem.

Why We Built This in Sanity

Harvest is a great platform, especially for quickly starting with time tracking. However, for our use case, we needed something more flexible, scalable, and deeply integrated with our existing infrastructure.

What We Needed

The Sanity Advantage

Sanity provides a programmable content platform that lets developers build custom workflows and applications while giving teams a unified system for all their data.

What Sanity gives us:

How We Broke Down Harvest’s Core Functionality

Before writing a single line of code, we spent time carefully studying how Harvest handles time tracking. Not just at the surface level, but at the workflow level — what a user experiences, what managers need, and how everything stays connected.

Instead of copying features, we deconstructed the product into building blocks.

1. User & Authentication Management

Security and access control form the foundation of any team tool. We needed a robust system that could handle different permission levels while keeping the login experience simple. Google OAuth provides the convenience users expect, while our pre-invite model ensures only authorized team members can access the system.

This controls who can access the system and what they can do.

2. Client Management

Every billable hour needs to be associated with a client for accurate invoicing. Our client management system serves as the top level of our project hierarchy, connecting companies to projects to time entries. This structure enables powerful reporting by clients and ensures all work is properly attributed.

Clients form the foundation for project billing.

3. Project Management

Projects are where the actual work happens. Each project connects a client to specific tasks and team members, creating a complete picture of what’s being worked on and by whom. Our project management module handles everything from initial setup through completion, with budget tracking to prevent overruns.

Projects organize all billable work.

4. Task Management

Tasks provide the granular detail needed for accurate time tracking and reporting. Rather than creating tasks for each project individually, we built a reusable task library that can be assigned across multiple projects. This ensures consistency in how time is categorized and makes reporting more meaningful.

Tasks provide granular tracking within projects.

5. Time Entry Workflow

This is the core of the entire system — where team members actually log their hours. We replicated Harvest’s intuitive flow where entering time feels effortless.

The workflow moves from entry to submission to approval, with clear states at each stage so everyone knows where things stand.

6. Team Management

Managers need visibility into their team’s work without micromanaging. Our team management module provides at-a-glance statistics, historical data, and the ability to drill into individual member details. This enables informed decision-making about resource allocation and project staffing.

Team management provides oversight and accountability.

7. Approval Workflow

The approval system bridges the gap between time entry and billing. Without proper approvals, it’s difficult to ensure accuracy and prevent disputes. Our three-state workflow makes it clear what’s been reviewed and what’s still pending, while preventing accidental edits to approved time.

Three-state system:

StateDescription
UnsubmittedUser can freely edit entries
PendingSubmitted for approval, awaiting manager review
ApprovedLocked, entries cannot be edited

Managers review pending entries and approve at the week level.

8. Reporting & Analytics

Raw time data is only valuable when you can analyze and act on it. Our reporting module transforms individual time entries into meaningful insights about project profitability, team utilization, and billing accuracy. The ability to export data ensures compatibility with existing accounting and invoicing systems.

Reports turn raw time data into actionable insights.

Core Components & Building Blocks Used in Sanity

Sanity provides a modular architecture made up of powerful building blocks that allow us to design highly customized systems. Understanding these components is essential for anyone considering Sanity as the foundation for a business application.

1. Content Lake (The Foundation)

The Content Lake is Sanity’s real-time database that stores all our structured content. Unlike traditional databases, it’s designed for collaboration and provides instant synchronization across all connected clients. Every time entry, project update, and user change is immediately available throughout the system.

What it gives us:

2. Schema System

Sanity’s schema system is where we define the structure of our data. Each schema acts as a blueprint that enforces data consistency, provides validation, and establishes relationships between different entity types. This strong typing prevented countless bugs and made the codebase more maintainable.

Our 11 Document Types:

SchemaPurpose
userTeam members with roles, capacity, rates
clientClient companies with contacts
projectProjects linked to clients with budgets
taskReusable tasks for time tracking
categoryTask categories (Development, Design, etc.)
timesheetIndividual time records
teamTeam groupings for managers
contactEmbedded client contact type
jobcategoryUser designations (Developer, PM)
uiSettingsTheme and customization
reportSaved report configurations

3. GROQ Query Language

GROQ (Graph-Relational Object Queries) is Sanity’s powerful query language. It enabled us to fetch complex, nested data in single requests — something that would require multiple queries in a traditional REST API. This dramatically simplified our frontend code and improved performance.

4. APIs & Integration

Sanity provides a comprehensive set of APIs that makes integration with our Next.js frontend straightforward. The JavaScript SDK handles authentication, caching, and real-time updates automatically, letting us focus on building features rather than infrastructure.

Step-by-Step Implementation Overview

Step 1: Tech Stack & Architecture

Choosing the right tech stack was crucial for development speed and long-term maintainability. We selected technologies that work well together and have strong community support. This combination allowed us to move quickly while building a production-ready application.

Technology Stack:

LayerTechnology
Frontend FrameworkNext.js 14 (App Router)
UI LibraryReact 18
StylingTailwind CSS
CMS/DatabaseSanity.io v3
AuthenticationNextAuth.js + Google OAuth
ChartsRecharts
Date Handlingdate-fns
ExportXLSX library
NotificationsReact Hot Toast

Architecture:

Next.js Frontend (Top Layer): The user interface layer built with Next.js 14 App Router. This handles all user interactions, displays data, and manages client-side state. All modules — Dashboard, Timesheets, Team Management, Projects, and Reports — are rendered here.

API Routes (Middle Layer): Next.js API routes serve as the backend logic layer. They handle business operations like creating/updating time entries, managing team data, performing authentication checks, and generating exports. This layer validates requests, enforces permissions before storage.

Sanity Content Lake (Data Layer): Sanity’s real-time database stores all structured content. It houses all document types (Users, Projects, Clients, Tasks, Time Entries) and provides real-time synchronization and powerful GROQ querying capabilities.

NextAuth + Google OAuth (Authentication Layer): Handles all user authentication and session management. Google OAuth provides seamless login, while NextAuth manages JWT sessions and role-based access control throughout the application.

How They Work Together:

  1. User interacts with the Next.js frontend
  2. Frontend makes requests to API routes for data operations
  3. API routes authenticate via NextAuth and query/update Sanity
  4. Sanity stores data and syncs changes in real-time
  5. Updates flow back through the API to the frontend

This layered architecture provides clear separation of concerns: frontend handles UI, API handles business logic, Sanity manages data, and NextAuth secures access.

Step 2: Content Modeling in Sanity

We designed a fully structured content system using custom schemas that define how every piece of data is created, validated, and connected. Getting the schema design right was the most important architectural decision — it affected everything from query performance to UI complexity.

Client Schema:

Clients anchor our billing structure. The contacts array with primary contact logic ensures there’s always a clear point of contact.

Here is the code snippet for client schema:

import { defineField, defineType } from 'sanity'
import { FiBriefcase } from 'react-icons/fi'

export default defineType({
  name: 'client',
  title: 'Client',
  type: 'document',
  icon: FiBriefcase,
  fields: [
    defineField({
      name: 'name',
      title: 'Client Name',
      type: 'string',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'name',
        maxLength: 96,
      },
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'contacts',
      title: 'Contacts',
      type: 'array',
      of: [
        {
          type: 'contact',
        },
      ],
    }),
    defineField({
      name: 'address',
      title: 'Address',
      type: 'text',
      rows: 3,
    }),
    defineField({
      name: 'preferredCurrency',
      title: 'Preferred Currency',
      type: 'string',
      options: {
        list: [
          { title: 'USD ($)', value: 'USD' },
          { title: 'EUR (€)', value: 'EUR' },
          { title: 'GBP (£)', value: 'GBP' },
          { title: 'JPY (¥)', value: 'JPY' },
          { title: 'CAD (C$)', value: 'CAD' },
          { title: 'AUD (A$)', value: 'AUD' },
          { title: 'CHF (CHF)', value: 'CHF' },
          { title: 'CNY (¥)', value: 'CNY' },
          { title: 'SEK (kr)', value: 'SEK' },
          { title: 'NZD (NZ$)', value: 'NZD' },
        ],
      },
      validation: (Rule) => Rule.required(),
      initialValue: 'USD',
    }),
    defineField({
      name: 'isActive',
      title: 'Active',
      type: 'boolean',
      initialValue: true,
    }),
    defineField({
      name: 'isArchived',
      title: 'Archived',
      type: 'boolean',
      initialValue: false,
    }),
    defineField({
      name: 'createdAt',
      title: 'Created At',
      type: 'datetime',
      initialValue: () => new Date().toISOString(),
    }),
  ],
  preview: {
    select: {
      title: 'name',
      contacts: 'contacts',
    },
    prepare(selection) {
      const { title, contacts } = selection
      const primaryContact = contacts?.find((contact: any) => contact.isPrimary)
      const primaryName = primaryContact
        ? `${primaryContact.firstName} ${primaryContact.lastName}`.trim()
        : null

      return {
        title,
        subtitle: primaryName ? `Primary: ${primaryName}` : 'No primary contact',
      }
    },
  },
})


User Schema:

The user schema captures everything needed for team management and billing calculations.

Project Schema:

Projects tie together clients, team members, and tasks into a cohesive unit. The assigned users array uses Sanity’s reference filtering to only show eligible team members, preventing assignment errors.

Timesheet Schema:

The timesheet schema is the most frequently written document in the system. We optimized it for quick writes while maintaining the relationships needed for reporting.

Full schemas available in GitHub repository

Step 3: Building the User Interface

We built a comprehensive admin interface tailored to different user roles. Each module was designed with its primary users in mind — team members need speed, managers need oversight, and admins need control.

3.1 Time Tracking Module

The time tracking module is where users spend most of their time. The week view provides the familiar Harvest-style grid, while the day view offers more detail when needed.

Week View Timesheet:

The week view displays a 7-day grid where each row represents a project-task combination. Users can quickly scan their week, enter hours in any cell, and see totals update in real-time. This mirrors Harvest’s core experience that our team already loved.

Day View Timesheet:

The day view provides a focused look at a single day’s entries with more detail visible. It’s ideal for reviewing or making detailed edits to entries, especially when notes and additional context are important.

Key Features:

These features combine to create a time entry experience that feels instant and forgiving. Multiple input formats accommodate different user preferences, while optimistic updates ensure the interface never feels slow.

3.2 Team Management Module

Team management gives managers and admins visibility into their organization’s time tracking. The overview provides at-a-glance statistics, while the detail view enables deeper investigation when something looks off.

Team Overview:

The team overview displays all team members with their weekly statistics. Managers can quickly identify who’s tracking time, who’s falling behind, and who might need support. Historical navigation allows us to review past weeks for trend analysis.

Member Details:

Drilling into a specific team member reveals their full time entry history, capacity utilization, and profile information. Admins can edit entries directly when corrections are needed, maintaining data accuracy.

3.3 Project Management Module

Projects are the organizing principle for all billable work. This module handles the full project lifecycle from creation through completion, with budget tracking to prevent overruns before they happen.

Project List:

The project list shows all projects with their current status, associated client, and key metrics. At a glance, admins can see which projects are active, how much time has been logged, and whether budgets are on track.

Project Details:

The detail view provides complete project configuration. Admins can manage team assignments, configure budgets, and review all time logged to the project. This single view contains everything needed for project oversight.

3.4 Management Module

The Management Module provides a centralized interface for managing clients, tasks, and roles. This unified module ensures that all organizational data — clients, task modules, and user roles — can be managed efficiently from a single location with tabbed navigation.

Client Tab:

Clients sit at the top of our billing hierarchy. Managing client information accurately ensures that all downstream data — projects, time entries, invoices — flows correctly through the system.

Task Tab:

Tasks provide the granular detail needed for accurate time tracking and reporting. The task management interface allows admins to create reusable tasks, organize them by categories, and manage their assignment across projects.

Role Tab:

Roles (job categories) define team member designations and help organize the organization structure. The role management interface enables admins to create roles, assign team members to roles, and manage role assignments across the organization.

3.5 Reporting Module

Reports transform raw time data into actionable business insights. Whether tracking project profitability, team utilization, or billing accuracy, our reporting module provides the answers.

Time Reports:

The time reports interface offers flexible filtering and grouping options. Users can slice data by any dimension — date, client, project, or team member — to answer specific business questions.

Visualizations:

Charts and metrics make patterns visible at a glance. Rather than scanning rows of numbers, managers can quickly identify trends and anomalies.

Export Options:

Data export ensures our system integrates with existing accounting and invoicing workflows. Both CSV and formatted Excel outputs accommodate different downstream systems.

3.6 Account Profiles & Settings Module

The Account Profiles & Settings Module provides users with comprehensive control over their personal account information, preferences, and notification settings.

Basic Info Tab:

Users can manage their core profile information, including personal details, avatar upload, timezone settings, and billing rates (for admins).

Assigned Projects Tab:

Users can view all projects they’re assigned to, providing visibility into their current workload and project responsibilities. This helps team members understand their project assignments at a glance.

Assigned People Tab:

Managers and admins can view and manage the people assigned to them, providing oversight of their team structure and relationships.

Permissions Tab:

Users can view their account permissions, which control what actions they can perform within the system. Permissions are role-based and determine access to various features and modules.

Notifications Tab:

The notifications system allows users to customize their email and reminder preferences, ensuring they receive relevant updates without being overwhelmed.

Step 4: Authentication & Authorization

Security was a primary concern from day one. Our authentication system balances ease of use with strict access control, ensuring only authorized users can access appropriate functionality.

Google OAuth Implementation:

We chose Google OAuth for its simplicity and security. By requiring pre-invitation, we ensure that only intended users can access the system — no one can sign up simply by having a Google account. Account linking connects new OAuth logins with existing user records.

Role-Based Access:

Three distinct roles provide the right capabilities to the right people. Each role is designed for specific organizational needs, from individual contributors to team leaders to administrators. Every API route and UI element respects these permissions.

Admin Role:

Administrators have complete control over the system. This role is designed for those who need full access to manage the entire organization’s time tracking infrastructure.

Admin Capabilities:

Manager Role:

Managers have elevated permissions focused on team oversight and project management. They can manage their assigned teams, approve time entries, and access comprehensive reporting for their projects and people.

Manager Capabilities:

User Role:

Users are the core contributors who track their time. This role is designed for team members who primarily log hours and view their own data. It provides a focused, streamlined experience without administrative complexity.

User Capabilities:

Permission Matrix:

CapabilityAdminManagerUser
Track own time
View own reports
View team timesheets✅ (own team)
Approve entries✅ (own team)
Manage projects✅ (filtered)
Manage clients
Manage tasks
Manage roles
View all reports✅ (filtered)
Access UI Settings

Step 5: API Implementation

Our API layer handles all data operations between the frontend and Sanity. We built RESTful endpoints for each major entity, with proper authentication checks and error handling on every route.

Core API Routes:

Each endpoint follows consistent patterns for request/response handling, making the API predictable and easy to work with. Authentication is verified on every request, and role-based permissions determine what data is accessible.

EndpointMethodsPurpose
/api/timesheetsGET, POST, PUTTime entry CRUD
/api/timesheets/[id]PATCH, DELETESingle entry operations
/api/team/weeklyGETTeam weekly stats
/api/team/members/[id]GET, PUT, DELETEMember operations
/api/team/pending-approvalsGET, POSTApproval workflow
/api/projectsGET, POSTProject management
/api/clientsGET, POSTClient management
/api/reportsGETReport data

Key Decisions, Tradeoffs, and Learnings

Every project involves difficult choices. Here are the key decisions we made, the tradeoffs we accepted, and what we learned from each.

1. Build vs Buy: Choosing Sanity Over SaaS

2. Week-Level Approval vs Individual Entry Approval

3. Pre-Invited Users Only

4. Sanity as Database vs Traditional Database

5. Optimistic UI for Time Entry

Final Thoughts

Building a Harvest-like time tracking tool doesn’t require years of work. It requires clear data structure, strong fundamentals, and disciplined execution.

We built this platform in less than 3 months to prove a simple point: when your content model is right, everything else moves faster.

Instead of obsessing over features, focus on fundamentals. These principles guided our development and will serve any similar project well:

Get the Code

Ready to explore the implementation details? The complete source code is available for review and reference.

The repo includes:

Exit mobile version