Soft Deletes

Soft deletes mark records as deleted without removing them from the database.

Soft Deletes

Soft deletes mark records as deleted without removing them from the database.

Auto-Detection

Add a deletedAt column to your schema:

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  // ... other fields
  deletedAt: integer('deleted_at', { mode: 'timestamp' }),
})

Supported column names (auto-detected):

  • deletedAt
  • deleted_at
  • deletedDate

Behavior

Delete Operation

DELETE /api/posts/123

With deletedAt column:

{
  "success": true,
  "softDeleted": true,
  "message": "Record marked as deleted"
}

Without deletedAt column:

{
  "success": true,
  "softDeleted": false,
  "message": "Record permanently deleted"
}

List Operation

Soft-deleted records are automatically filtered:

GET /api/posts
// Returns only non-deleted posts

Include Deleted (Admin Only)

GET /api/posts?includeDeleted=true
// Requires 'admin' permission

Get Operation

Soft-deleted records return 404 (unless admin):

GET /api/posts/123
// 404 if deleted (unless user has 'admin' permission)

Restore Endpoint

Restore soft-deleted records:

POST /api/posts/123/restore

Response:

{
  "data": { ... },
  "restored": true
}

Requirements:

  • User must have admin permission (by default)
  • Record must be soft-deleted
  • Table must support soft deletes

Custom Authorization

Override restore authorization in a custom handler:

// server/api/posts/[id]/restore.post.ts
import { defineAutoApiHandler } from '@websideproject/nuxt-auto-api/utils'

export default defineAutoApiHandler({
  async execute(context) {
    const postId = context.params.id
    const post = await context.db.query.posts.findFirst({
      where: and(
        eq(posts.id, postId),
        isNotNull(posts.deletedAt)
      ),
    })

    // Custom authorization: allow post owner to restore
    if (post.userId !== context.user.id && !context.permissions.includes('admin')) {
      throw createError({ statusCode: 403 })
    }

    // Restore
    const [restored] = await context.db
      .update(posts)
      .set({ deletedAt: null })
      .where(eq(posts.id, postId))
      .returning()

    return { data: restored, restored: true }
  },
})

Migration Example

Add deletedAt to existing table:

// migrations/0001_add_soft_delete.sql
ALTER TABLE posts ADD COLUMN deleted_at INTEGER;
CREATE INDEX idx_posts_deleted_at ON posts(deleted_at);

Best Practices

  1. Index deletedAt for query performance
  2. Use for user data - allows recovery if needed
  3. Purge old deleted records - periodic cleanup job
  4. Document restore policy - who can restore, when to purge
  5. Consider GDPR - may need hard delete for user requests

Hard Delete Override

Force hard delete even with deletedAt column:

// Custom handler for permanent deletion
export default defineAutoApiHandler({
  async execute(context) {
    const postId = context.params.id

    // Permanently delete (ignore soft delete)
    await context.db.delete(posts).where(eq(posts.id, postId))

    return { success: true, hardDeleted: true }
  },
})

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.