Email Archive Feature Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add archive functionality to email schedules — auto-archive terminal-state one-time emails, add Archive tab, remove Sent tab, keep Logs page as-is.
Architecture: Database trigger auto-archives one-time emails when they reach terminal status (sent/failed/cancelled). New is_archived boolean column on email_schedules. Client-side filtering updated to replace "Sent" tab with "Archived" tab. "All" tab excludes archived.
Tech Stack: PostgreSQL (trigger + migration), TypeScript, React, Supabase client
Design doc: docs/plans/2026-02-09-email-archive-design.md
Task 1: Database Migration
Files:
- Create:
supabase/migrations/20260211000000_add_email_archive.sql
Step 1: Create the migration file
-- Add is_archived column
ALTER TABLE email_schedules
ADD COLUMN is_archived boolean NOT NULL DEFAULT false;
-- Backfill: archive existing terminal one-time emails
UPDATE email_schedules
SET is_archived = true
WHERE mode IN ('manual', 'scheduled')
AND status IN ('sent', 'failed', 'cancelled');
-- Auto-archive trigger function
CREATE OR REPLACE FUNCTION auto_archive_email_schedule()
RETURNS TRIGGER AS $$
BEGIN
-- Only auto-archive one-time emails (manual/scheduled) when they reach terminal status
IF NEW.mode IN ('manual', 'scheduled')
AND NEW.status IN ('sent', 'failed', 'cancelled')
AND (OLD.status IS DISTINCT FROM NEW.status)
THEN
NEW.is_archived := true;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Attach trigger
DROP TRIGGER IF EXISTS trg_auto_archive_email_schedule ON email_schedules;
CREATE TRIGGER trg_auto_archive_email_schedule
BEFORE UPDATE ON email_schedules
FOR EACH ROW
EXECUTE FUNCTION auto_archive_email_schedule();
-- Index for efficient filtering
CREATE INDEX idx_email_schedules_is_archived ON email_schedules(is_archived);Step 2: Push migration to remote database
Run: pnpm supabase:push Expected: Migration applied successfully
Step 3: Regenerate TypeScript types
Run: pnpm supabase:types Expected: src/lib/supabaseTypes.ts updated with is_archived column in email_schedules table type
Step 4: Commit
git add supabase/migrations/20260211000000_add_email_archive.sql src/lib/supabaseTypes.ts
git commit -m "feat(email): add is_archived column with auto-archive trigger"Task 2: Update TypeScript Types
Files:
- Modify:
src/features/email-schedules/types.ts
Step 1: Add isArchived to EmailSchedule interface
In src/features/email-schedules/types.ts, find the EmailSchedule interface (line 40–66). Add isArchived after nextSendAt:
export interface EmailSchedule {
// ... existing fields ...
nextSendAt: string | null;
isArchived: boolean; // <-- ADD THIS
}Step 2: Verify build compiles
Run: pnpm build:check Expected: Build errors in EmailScheduleService.ts mapRow() (missing isArchived mapping) — this is expected and will be fixed in Task 3.
Step 3: Commit
git add src/features/email-schedules/types.ts
git commit -m "feat(email): add isArchived to EmailSchedule type"Task 3: Update Service Layer
Files:
- Modify:
src/features/email-schedules/services/EmailScheduleService.ts
Step 1: Add isArchived mapping in mapRow()
In EmailScheduleService.ts, find the mapRow() function (line 37–64). Add isArchived mapping after nextSendAt (line 62):
function mapRow(row: EmailScheduleRow): EmailSchedule {
return {
// ... existing fields ...
nextSendAt: row.next_send_at,
isArchived: row.is_archived ?? false, // <-- ADD THIS
};
}Step 2: Add setArchived() method to EmailScheduleService
Add a new method to the EmailScheduleService object. Place it after the remove() method (after line 363), before sendNow():
async setArchived(id: string, isArchived: boolean): Promise<EmailSchedule> {
const { data, error } = await supabase
.from("email_schedules")
.update({ is_archived: isArchived })
.eq("id", id)
.select("*")
.single()
.returns<EmailScheduleRow>();
if (error) {
console.error("Failed to update archive status", error);
throw new Error(error.message || "Failed to update archive status");
}
return mapRow(data);
},Step 3: Verify build compiles
Run: pnpm build:check Expected: PASS (no type errors in service layer)
Step 4: Commit
git add src/features/email-schedules/services/EmailScheduleService.ts
git commit -m "feat(email): add isArchived mapping and setArchived service method"Task 4: Update Hook
Files:
- Modify:
src/features/email-schedules/hooks/useEmailSchedules.ts
Step 1: Add archiveSchedule and unarchiveSchedule callbacks
In useEmailSchedules.ts, add two new callbacks after deleteSchedule (after line 56):
const archiveSchedule = useCallback(async (id: string) => {
const updated = await EmailScheduleService.setArchived(id, true);
setSchedules((prev) =>
prev.map((item) => (item.id === id ? updated : item))
);
}, []);
const unarchiveSchedule = useCallback(async (id: string) => {
const updated = await EmailScheduleService.setArchived(id, false);
setSchedules((prev) =>
prev.map((item) => (item.id === id ? updated : item))
);
}, []);Step 2: Expose new callbacks in return object
Update the return object (line 94–103) to include the new callbacks:
return {
schedules,
loading,
error,
refresh,
createSchedule,
updateSchedule,
deleteSchedule,
sendNow,
archiveSchedule, // <-- ADD
unarchiveSchedule, // <-- ADD
};Step 3: Verify build compiles
Run: pnpm build:check Expected: PASS
Step 4: Commit
git add src/features/email-schedules/hooks/useEmailSchedules.ts
git commit -m "feat(email): add archive/unarchive callbacks to useEmailSchedules hook"Task 5: Update EmailSchedulesTable Component — Filters & Tabs
Files:
- Modify:
src/features/email-schedules/components/EmailSchedulesTable.tsx
Step 1: Add onArchive and onUnarchive to Props type
In EmailSchedulesTable.tsx, update the Props type (line 5–18):
type Props = {
schedules: EmailSchedule[];
senderMap: Map<string, string>;
loading: boolean;
error: string | null;
sendingId: string | null;
deletingId: string | null;
onEdit: (schedule: EmailSchedule) => void;
onView: (schedule: EmailSchedule) => void;
onSendNow: (id: string) => void;
onDelete: (id: string) => void;
onDuplicate: (schedule: EmailSchedule) => void;
onRefresh: () => void;
onArchive: (id: string) => void; // <-- ADD
onUnarchive: (id: string) => void; // <-- ADD
};Step 2: Update FilterStatus type
Replace line 87:
Old:
type FilterStatus = "all" | "pending" | "sent" | "failed" | "recurring";New:
type FilterStatus = "all" | "pending" | "failed" | "recurring" | "archived";Step 3: Destructure new props in component
Update the component destructuring (line 112–125) to include onArchive and onUnarchive:
export function EmailSchedulesTable({
schedules,
senderMap,
loading,
error,
sendingId,
deletingId,
onEdit,
onView,
onSendNow,
onDelete,
onDuplicate,
onRefresh,
onArchive, // <-- ADD
onUnarchive, // <-- ADD
}: Props) {Step 4: Update filteredSchedules logic
Replace the entire filteredSchedules useMemo (lines 128–147):
const filteredSchedules = useMemo(() => {
if (filterStatus === "all")
return schedules.filter((s) => !s.isArchived);
if (filterStatus === "pending") {
return schedules.filter(
(s) =>
!s.isArchived &&
["draft", "scheduled", "processing"].includes(s.status)
);
}
if (filterStatus === "failed") {
return schedules.filter(
(s) =>
!s.isArchived && ["failed", "cancelled"].includes(s.status)
);
}
if (filterStatus === "recurring") {
return schedules.filter((s) => s.mode === "recurring");
}
if (filterStatus === "archived") {
return schedules.filter((s) => s.isArchived);
}
return schedules;
}, [schedules, filterStatus]);Step 5: Update counts logic
Replace the entire counts useMemo (lines 149–161):
const counts = useMemo(() => {
return {
all: schedules.filter((s) => !s.isArchived).length,
pending: schedules.filter(
(s) =>
!s.isArchived &&
["draft", "scheduled", "processing"].includes(s.status)
).length,
failed: schedules.filter(
(s) =>
!s.isArchived && ["failed", "cancelled"].includes(s.status)
).length,
recurring: schedules.filter((s) => s.mode === "recurring").length,
archived: schedules.filter((s) => s.isArchived).length,
};
}, [schedules]);Step 6: Update filter buttons
Replace the filter buttons block (lines 180–219). Remove the "Sent" button, add "Archived" button:
<FilterButton
active={filterStatus === "all"}
count={counts.all}
label="All"
onClick={() => {
setFilterStatus("all");
}}
/>
<FilterButton
active={filterStatus === "pending"}
count={counts.pending}
label="Pending"
onClick={() => {
setFilterStatus("pending");
}}
/>
<FilterButton
active={filterStatus === "failed"}
count={counts.failed}
label="Failed"
onClick={() => {
setFilterStatus("failed");
}}
/>
<FilterButton
active={filterStatus === "recurring"}
count={counts.recurring}
label="Recurring"
onClick={() => {
setFilterStatus("recurring");
}}
/>
<FilterButton
active={filterStatus === "archived"}
count={counts.archived}
label="Archived"
onClick={() => {
setFilterStatus("archived");
}}
/>Step 7: Verify build compiles
Run: pnpm build:check Expected: Build error in index.tsx (missing onArchive/onUnarchive props) — expected, fixed in Task 7.
Step 8: Commit
git add src/features/email-schedules/components/EmailSchedulesTable.tsx
git commit -m "feat(email): update table filters — replace Sent with Archived tab"Task 6: Update EmailSchedulesTable Component — Row Actions
Files:
- Modify:
src/features/email-schedules/components/EmailSchedulesTable.tsx
Step 1: Update row actions for non-pending emails
Find the row actions section for non-pending emails (lines 400–421). This currently shows "Resend" and "Duplicate". Replace with logic that shows Archive/Unarchive based on isArchived state:
Replace lines 400–421:
) : (
<>
{schedule.isArchived ? (
<button
className="btn btn-sm bg-primary text-white"
type="button"
onClick={() => {
onUnarchive(schedule.id);
}}
>
Unarchive
</button>
) : (
<>
<button
className="btn btn-sm bg-primary text-white"
type="button"
onClick={() => {
onEdit(schedule);
}}
>
Resend
</button>
<button
className="btn btn-sm border border-default-200 text-default-700"
type="button"
onClick={() => {
onDuplicate(schedule);
}}
>
Duplicate
</button>
<button
className="btn btn-sm border border-default-200 text-amber-700"
type="button"
onClick={() => {
onArchive(schedule.id);
}}
>
Archive
</button>
</>
)}
</>
)}Step 2: Verify build compiles
Run: pnpm build:check Expected: Build error in index.tsx (missing props) — expected, fixed in Task 7.
Step 3: Commit
git add src/features/email-schedules/components/EmailSchedulesTable.tsx
git commit -m "feat(email): add archive/unarchive row actions"Task 7: Update Page Component — Wire Everything Together
Files:
- Modify:
src/app/(admin)/(pages)/email-schedules/index.tsx
Step 1: Destructure new hook callbacks
In index.tsx, update the useEmailSchedules destructuring (lines 179–188):
const {
schedules,
loading,
error,
refresh,
createSchedule,
updateSchedule,
deleteSchedule,
sendNow,
archiveSchedule, // <-- ADD
unarchiveSchedule, // <-- ADD
} = useEmailSchedules(Boolean(user));Step 2: Create handler functions
Add handleArchive and handleUnarchive handlers after handleDuplicate (after line 413):
const handleArchive = useCallback(
async (id: string) => {
try {
await archiveSchedule(id);
toast.success("Email archived");
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to archive email"
);
}
},
[archiveSchedule]
);
const handleUnarchive = useCallback(
async (id: string) => {
try {
await unarchiveSchedule(id);
toast.success("Email unarchived");
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to unarchive email"
);
}
},
[unarchiveSchedule]
);Step 3: Update stats — replace sentCount with archivedCount
Replace the sentCount useMemo (lines 225–228):
Old:
const sentCount = useMemo(
() => schedules.filter((s) => s.status === "sent").length,
[schedules]
);New:
const archivedCount = useMemo(
() => schedules.filter((s) => s.isArchived).length,
[schedules]
);Step 4: Update stats card in JSX
Replace the "Sent" stats card (lines 582–598):
Old:
<div className="card group cursor-pointer transition-all duration-200 hover:shadow-md hover:border-primary/20">
<div className="card-body py-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-default-500">
Sent
</p>
<p className="text-2xl font-bold text-default-800">
{sentCount}
</p>
</div>
<div className="rounded-lg bg-emerald-100 p-2.5 text-emerald-600">
<PaperAirplaneIcon />
</div>
</div>
</div>
</div>New:
<div className="card group cursor-pointer transition-all duration-200 hover:shadow-md hover:border-primary/20">
<div className="card-body py-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-default-500">
Archived
</p>
<p className="text-2xl font-bold text-default-800">
{archivedCount}
</p>
</div>
<div className="rounded-lg bg-default-100 p-2.5 text-default-500">
<EnvelopeIcon />
</div>
</div>
</div>
</div>Step 5: Pass new props to EmailSchedulesTable
Update the <EmailSchedulesTable> rendering (lines 667–680) to include the new props:
<EmailSchedulesTable
deletingId={deletingId}
error={error}
loading={loading}
schedules={schedules}
senderMap={senderMap}
sendingId={sendingId}
onArchive={handleArchive}
onDelete={handleDelete}
onDuplicate={handleDuplicate}
onEdit={handleEdit}
onRefresh={() => void refresh()}
onSendNow={handleSendNow}
onUnarchive={handleUnarchive}
onView={setViewingSchedule}
/>Step 6: Update scheduledCount to exclude archived
Update the scheduledCount useMemo (lines 217–223) to exclude archived emails:
const scheduledCount = useMemo(
() =>
schedules.filter(
(s) =>
!s.isArchived &&
["draft", "scheduled", "processing"].includes(s.status)
).length,
[schedules]
);Step 7: Verify full build
Run: pnpm build:check Expected: PASS — no type errors
Step 8: Commit
git add src/app/\(admin\)/\(pages\)/email-schedules/index.tsx
git commit -m "feat(email): wire archive/unarchive to page component, update stats"Task 8: Lint & Final Verification
Step 1: Run linter
Run: pnpm lint Expected: PASS (or only pre-existing warnings, no new errors)
Step 2: Fix any lint issues
Run: pnpm lint:fix if needed
Step 3: Run full build
Run: pnpm build:check Expected: PASS
Step 4: Run unit tests
Run: pnpm test:unit Expected: PASS (existing tests should still pass; isArchived defaults to false so existing mock data should be compatible)
Step 5: Final commit if any lint fixes
git add -A
git commit -m "chore: lint fixes for email archive feature"Summary of All Changes
| Task | Files | Description |
|---|---|---|
| 1 | supabase/migrations/20260211000000_add_email_archive.sql, src/lib/supabaseTypes.ts | DB migration + type regen |
| 2 | src/features/email-schedules/types.ts | Add isArchived to interface |
| 3 | src/features/email-schedules/services/EmailScheduleService.ts | Add mapping + setArchived() method |
| 4 | src/features/email-schedules/hooks/useEmailSchedules.ts | Add archive/unarchive callbacks |
| 5 | src/features/email-schedules/components/EmailSchedulesTable.tsx | Update filters, counts, tabs |
| 6 | src/features/email-schedules/components/EmailSchedulesTable.tsx | Update row actions |
| 7 | src/app/(admin)/(pages)/email-schedules/index.tsx | Wire everything, update stats |
| 8 | All files | Lint + final verification |