---
term: 'Database Triggers (BeforeSave and AfterSave)'
seoTitle: 'Database Triggers & beforeSave/afterSave Hooks Explained'
headline: 'What are Database Triggers (BeforeSave and AfterSave)?'
slug: database-triggers-beforesave-aftersave
category: backend-compute
shortDefinition: 'A database trigger is a code hook that runs automatically on data events — before a save to validate, after it to react.'
relatedTerms:
  - cloud-code-serverless-functions
  - webhooks
  - crud-operations
  - real-time-live-queries
contrastsWith:
  - webhooks
aboutTerms:
  - 'beforeSave'
  - 'afterSave'
  - 'BEFORE/AFTER Triggers'
faq:
  - question: 'What is a database trigger?'
    answer: 'Procedural code that executes automatically when a data event — insert, update, delete — occurs on a specific table or class. The classical form lives inside the SQL database; the modern application-level form is a hook function like beforeSave or afterSave that the backend runs around every write.'
  - question: 'What are the types of database triggers?'
    answer: 'Three axes: timing (BEFORE the write, AFTER it, or INSTEAD OF it), event (insert, update, delete — some systems add schema and login events), and granularity (row-level, firing per affected row, versus statement-level, firing once per statement). Application hooks are effectively row-level BEFORE and AFTER triggers.'
  - question: 'What is the difference between BEFORE and AFTER triggers?'
    answer: 'Capability and timing. BEFORE runs ahead of the write and can validate, modify the incoming data, or abort the operation entirely. AFTER runs once the write succeeded — it cannot change what happened, only react: log it, count it, notify about it. AFTER never fires for writes that failed.'
  - question: 'What is the difference between a trigger and a stored procedure?'
    answer: 'Invocation. A stored procedure is called explicitly, takes parameters, and returns results. A trigger is never called — it fires automatically when its event occurs, parameterless, attached to a table. Same procedural machinery, opposite activation model.'
  - question: 'Should logic live in triggers or application code?'
    answer: 'The consensus is a hybrid: triggers (or hooks) for rules that must hold on every write path — validation, integrity, audit — and application services for complex workflows. Application-level hooks are the modern middle ground: trigger guarantees, real programming language, version control.'
  - question: 'When should you use triggers?'
    answer: 'Audit trails, integrity rules that must bind every client, computed and denormalized fields, timestamps, cascading cleanups. The discipline that keeps them healthy: short, simple, fast, and independent of deep business logic — a trigger is a rule, not a workflow engine.'
  - question: 'Do triggers hurt performance?'
    answer: 'They run synchronously inside the write path, so a slow trigger slows every save, and row-level triggers multiply on bulk operations — a hundred-thousand-row update fires a hundred thousand times. Keep before-hooks lean and push slow reactions to after-hooks or background jobs.'
  - question: 'Can a trigger cause an infinite loop?'
    answer: 'Famously — a trigger that writes to its own table re-fires itself, and an afterSave that saves the object it just handled recurses until something breaks. Guard by checking what actually changed before writing, and never re-save the triggering object from its own after-hook without a terminating condition.'
  - question: 'Can triggers call external services?'
    answer: 'SQL triggers essentially cannot — and should not — reach outside the database. This is the marquee advantage of application-level hooks: an afterSave in JavaScript can send a push notification, call any API, or fire an outgoing webhook, because it runs in the backend process with the full ecosystem available.'
  - question: 'What is beforeSave used for?'
    answer: 'The three jobs that require running before the write: validation (throw an error and the save fails, for every client), normalization (trim, truncate, canonicalize fields), and defaults or computed values. Side effects do not belong there — if the save later fails, the side effect already happened.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'PostgreSQL — CREATE TRIGGER'
    url: 'https://www.postgresql.org/docs/current/sql-createtrigger.html'
  - name: 'MySQL — Trigger Syntax and Examples'
    url: 'https://dev.mysql.com/doc/refman/8.4/en/trigger-syntax.html'
  - name: 'Cloud Code triggers guide'
    url: 'https://docs.parseplatform.org/cloudcode/guide/#beforesave-triggers'
  - name: 'Sequelize — Hooks lifecycle'
    url: 'https://sequelize.org/docs/v6/other-topics/hooks/'
  - name: 'Database trigger — Wikipedia'
    url: 'https://en.wikipedia.org/wiki/Database_trigger'
cta:
  title: 'Rules every write obeys'
  text: 'Register beforeSave and afterSave on any Back4app class and the rule binds every client — SDKs, REST, GraphQL, admin scripts — in JavaScript that can validate, normalize, count, notify, and call the outside world.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-07-30'
translationKey: database-triggers-beforesave-aftersave
---

**A database trigger is a code hook that runs automatically on data events — before a save to validate, after it to react.** The idea has two lineages sharing one principle: the classical **SQL trigger**, procedural code living inside the database engine, and the modern **application-level hook** — `beforeSave`, `afterSave`, `beforeDelete` — JavaScript registered per class in the backend. Both encode the same promise: the rule fires for *every* write, no matter which client, SDK, or script performed it — which is precisely what client-side validation can never promise.

## Key takeaways

| Question | Answer |
| --- | --- |
| The principle | Code bound to data events — automatic, per-table/class, every write path |
| Before = | Validate · normalize · default — can still change or **abort** the write |
| After = | React — count, notify, sync; the write already happened, so be idempotent |
| The two forms | SQL triggers inside the engine · JS hooks in the backend process |
| The classic bugs | Hidden logic · infinite self-firing loops · slow triggers blocking every save |

## The hook model in action

A validation rule and its enforcement, from both ends — the server defines it once, and every client everywhere inherits it:

**JavaScript:**

```javascript
// JavaScript — Cloud Code (cloud/main.js)
// beforeSave: validate + normalize — can still change or ABORT the write
Parse.Cloud.beforeSave('Review', (req) => {
  const stars = req.object.get('stars');
  if (stars < 1 || stars > 5) throw 'Stars must be between 1 and 5';
  const comment = req.object.get('comment');
  if (comment && comment.length > 500) {
    req.object.set('comment', comment.slice(0, 497) + '…');
  }
});

// afterSave: side effects — the write already happened; be idempotent
Parse.Cloud.afterSave('Review', async (req) => {
  if (req.object.existed()) return; // count only NEW reviews, once
  await updateAverageStars(req.object.get('movie'));
});
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// The hook fires for EVERY write path — this client included
final review = ParseObject('Review')
  ..set('movie', 'Arrival')
  ..set('stars', 9); // invalid — no client-side check needed
final response = await review.save();
print(response.error?.message); // "Stars must be between 1 and 5"
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection.
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// The hook fires for EVERY write path — this client included
var review = Review()
review.movie = "Arrival"
review.stars = 9 // invalid — no client-side check needed
do {
    _ = try await review.save()
} catch {
    print(error.localizedDescription) // "Stars must be between 1 and 5"
}
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection.
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// The hook fires for EVERY write path — this client included
val review = ParseObject("Review")
review.put("movie", "Arrival")
review.put("stars", 9) // invalid — no client-side check needed
try {
    review.save()
} catch (e: ParseException) {
    println(e.message) // "Stars must be between 1 and 5"
}
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection.
```

The full hook family extends the same shape across the data lifecycle: `beforeSave`/`afterSave`, `beforeDelete` (block deleting an Album that still has Photos) and `afterDelete` (clean up its children), plus `beforeFind`/`afterFind` to rewrite queries and strip fields from results — [CRUD](/glossary/crud-operations/), wrapped.

## The classical form: SQL triggers

```sql
CREATE TRIGGER audit_price_change
AFTER UPDATE ON products
FOR EACH ROW                              -- row-level: fires per affected row
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION log_price_change();      -- OLD and NEW hold both versions
```

The taxonomy every database shares, per the [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtrigger.html) and [MySQL](https://dev.mysql.com/doc/refman/8.4/en/trigger-syntax.html) references: **timing** — BEFORE (may modify `NEW` or abort), AFTER (reacts to the committed row), INSTEAD OF (replaces the operation, mainly for views); **event** — insert, update, delete; **granularity** — row-level versus statement-level (a 10-row update fires a row trigger 10 times, a statement trigger once). Two semantics worth engraving: SQL triggers run *inside the same transaction* as the write — a trigger failure rolls the whole operation back — and AFTER triggers only fire for writes that actually succeeded.

## Before vs. after: chosen by job

| | Before hooks | After hooks |
| --- | --- | --- |
| Can change the data | **Yes** — mutate, default, truncate | No — it's written |
| Can abort the write | **Yes** — throw, and the save fails | No |
| Right for | Validation, normalization, computed fields | Counters, notifications, sync, audit |
| Wrong for | **Side effects** — if the save fails later, the effect already fired | Anything that should have blocked the write |
| Failure behavior | Rejects the operation, error to the client | Often fire-and-forget — errors land in logs |
| Discipline | Fast — it blocks every save | **Idempotent** — it may run again |

The corollaries no explainer states: a side effect in a before-hook is a bug by construction (the email sends, then the save fails), and an after-hook that isn't idempotent is a duplicate counter waiting for a retry. In hook systems like Back4app's, `afterSave` completes after the client already got its response — reactions are asynchronous by design, so their failures must be tolerable and logged.

```mermaid
flowchart LR
  accTitle: Write lifecycle through before and after hooks
  accDescr: A write from any client first passes the before hook, which can validate, modify, or abort it. If allowed, the database commits the write, and the after hook then reacts with side effects such as counters, notifications, and webhooks, which must be idempotent because they may run more than once.
  C["Any client<br/>SDK · REST · script"] --> B{"beforeSave<br/>validate · normalize"}
  B -->|"throw"| X["Save rejected —<br/>error to the client"]
  B -->|"allow (possibly modified)"| W[("Write commits")]
  W --> A["afterSave<br/>side effects, idempotent"]
  A --> R["Counters · notifications ·<br/>outgoing webhooks"]
```

## Hooks vs. SQL triggers

| | Application hooks (beforeSave/afterSave) | SQL triggers |
| --- | --- | --- |
| Language | JavaScript + the full SDK and ecosystem | SQL / procedural SQL dialects |
| External calls | Yes — APIs, push, [webhooks](/glossary/webhooks/) | Essentially no — and shouldn't |
| Lives in | Your codebase: versioned, testable, deployed | The schema: inside the database |
| Transaction | Before-hooks gate the write; after-hooks run post-response | Same transaction — failure rolls back |
| Binds | Every request through the backend API | Every write to the table, from anything |
| Blind spot | Direct-to-database writes bypass it | Logic invisible to application debuggers |

The last row is the honest symmetry: each form's guarantee is scoped to its layer. A SQL trigger catches even a rogue psql session but hides logic from application tooling; an API-level hook binds every client path through the backend but not raw database access — which is why platforms that own the API gateway (all traffic flows through it) get the practical best of both. The ORM footnote belongs here too: [ORM hooks](https://sequelize.org/docs/v6/other-topics/hooks/) fire only through the ORM — bulk operations and raw SQL walk right past them.

## The pitfalls, honestly

**Hidden logic** is the classic: a developer debugs their own code for hours while a trigger silently rewrites values — triggers are invisible at the call site, so document them and keep them few. **Infinite loops**: a trigger writing to its own table (or an `afterSave` saving its own object) re-fires itself; guard on what changed, and give recursion a terminating condition before it finds one for you. **Synchronous cost**: before-hooks sit inside every write's latency — a 200 ms hook makes every save 200 ms slower, and bulk writes multiply row-level firings by the row count. **Cascade chains**: triggers that fire triggers that fire triggers turn one insert into an archaeology project. The umbrella rule from decades of practice: triggers enforce *rules*; the moment one starts orchestrating a *workflow*, move the workflow to [functions](/glossary/cloud-code-serverless-functions/) or [jobs](/glossary/background-jobs-task-schedulers/) and let the trigger just enqueue.

## Common use cases

- **Validation every client obeys** — the stars-between-1-and-5 rule, enforced against forged requests and future SDKs alike.
- **Audit trails** — who changed what, when, written by the event itself rather than by cooperative clients.
- **Denormalized counters and computed fields** — averages, counts, and search-friendly duplicates kept true at write time.
- **Cascading integrity** — refuse deletes with children, or clean the children up after.
- **Reactive side effects** — an `afterSave` firing a [push notification](/glossary/push-notifications-apns-fcm/) or an [outgoing webhook](/glossary/webhooks/): the database event becoming an integration event.

## Which hook where? A decision matrix

| The job | The hook |
| --- | --- |
| Reject bad data | beforeSave — throw |
| Trim, default, canonicalize | beforeSave — mutate |
| Update a counter or aggregate | afterSave — idempotently |
| Notify a person or system | afterSave → push / webhook / queue |
| Block dangerous deletes | beforeDelete |
| Clean up after deletes | afterDelete |
| Enforce per-user query scope | beforeFind |
| Long or slow reaction | afterSave *enqueues* a [job](/glossary/background-jobs-task-schedulers/) — never does the work inline |

## Limitations and trade-offs

- **Invisibility is the price of automaticity.** Logic nobody calls is logic nobody remembers; naming, docs, and code review keep the magic auditable.
- **Hooks scope to their layer.** API hooks miss direct DB writes; SQL triggers miss nothing but hide from your tooling — know which blind spot you chose.
- **Write latency is the budget.** Every before-hook spends it; measure hooks like you measure queries.
- **After-hooks are eventually consistent.** Counters lag by milliseconds and can double-fire — design reads (and retries) accordingly.
- **Triggers don't replace constraints.** Unique indexes, foreign keys, and [ACLs](/glossary/access-control-lists-acl/) enforce cheaper and earlier; triggers pick up where declarative rules stop.

## Triggers on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Triggers here are the data-event family of [Cloud Code](/glossary/cloud-code-serverless-functions/): register `Parse.Cloud.beforeSave('Review', …)` once — the code tabs show the whole pattern — and the rule binds every write arriving through REST, GraphQL, any SDK, or the dashboard, because the API gateway is the one path to the data. Hooks receive rich context (`request.object`, `request.original`, `request.user`, master-key status), so validation can differ for admins, `beforeFind` can scope queries per user on top of ACLs, and after-hooks have the full JavaScript ecosystem for the reactions SQL triggers can't reach — push notifications, outgoing webhooks, job enqueues. Deployed with your code, versioned in git, testable like any function: the trigger's guarantee, without the trigger's archaeology.
