Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPRECATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS21 | Config option `protectedFieldsOwnerExempt` defaults to `false` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS22 | Config option `protectedFieldsTriggerExempt` defaults to `true` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS23 | Config option `protectedFieldsSaveResponseExempt` defaults to `false` | | 9.7.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS24 | Config option `installation.duplicateDeviceTokenActionEnforceAuth` defaults to `true` | [#10451](https://github.com/parse-community/parse-server/pull/10451) | 9.9.0 (2026) | 10.0.0 (2027) | deprecated | - |

[i_deprecation]: ## "The version and date of the deprecation."
[i_change]: ## "The version and date of the planned change."
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ A big _thank you_ 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
- [Configuring File Adapters](#configuring-file-adapters)
- [Restricting File URL Domains](#restricting-file-url-domains)
- [Idempotency Enforcement](#idempotency-enforcement)
- [Installations](#installations)
- [Localization](#localization)
- [Pages](#pages)
- [Localization with Directory Structure](#localization-with-directory-structure)
Expand Down Expand Up @@ -658,6 +659,49 @@ Assuming the script above is named, `parse_idempotency_delete_expired_records.sh
2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1
```

## Installations

Parse Server deduplicates `_Installation` records when a new install collides with an existing row's `deviceToken`. The `installation` option block configures the dedup behavior.

### Options

| Parameter | Optional | Type | Default | Environment Variable |
|---|---|---|---|---|
| `installation.duplicateDeviceTokenActionEnforceAuth` | yes | `Boolean` | `false` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_ACTION_ENFORCE_AUTH` |
| `installation.duplicateDeviceTokenAction` | yes | `String` | `'delete'` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_ACTION` |
| `installation.duplicateDeviceTokenMergePriority` | yes | `String` | `'deviceToken'` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_MERGE_PRIORITY` |

#### `duplicateDeviceTokenActionEnforceAuth`

When `true`, the dedup operation runs with the caller's auth context so ACL and CLP are honored. When `false`, the dedup runs as master and bypasses both. Master and maintenance keys always bypass regardless of this flag.

#### `duplicateDeviceTokenAction`

What Parse Server does to the conflicting `_Installation` row(s) when a new install's `deviceToken` collides with an existing row.

- `'delete'`: destroys the conflicting row.
- `'update'`: clears the now-conflicting ID field on the conflicting row, preserving custom fields, channels, and history.

#### `duplicateDeviceTokenMergePriority`

When an existing row holds the new `deviceToken` but has no `installationId` of its own, Parse Server merges the two rows. This option controls which side wins.

- `'deviceToken'`: the deviceToken-only row survives; the request's installationId-matched row is the loser.
- `'installationId'`: the request's installationId-matched row survives; the deviceToken-only orphan is the loser.

### Configuration example

```javascript
const parseServer = new ParseServer({
...otherOptions,
installation: {
duplicateDeviceTokenActionEnforceAuth: true,
duplicateDeviceTokenAction: 'update',
duplicateDeviceTokenMergePriority: 'installationId',
},
});
```

## Localization

### Pages
Expand Down
2 changes: 2 additions & 0 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const nestedOptionTypes = [
'FileDownloadOptions',
'FileUploadOptions',
'IdempotencyOptions',
'InstallationOptions',
'Object',
'PagesCustomUrlsOptions',
'PagesOptions',
Expand All @@ -39,6 +40,7 @@ const nestedOptionEnvPrefix = {
FileDownloadOptions: 'PARSE_SERVER_FILE_DOWNLOAD_',
FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_',
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
InstallationOptions: 'PARSE_SERVER_INSTALLATION_',
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',
Expand Down
35 changes: 35 additions & 0 deletions spec/Deprecator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,39 @@ describe('Deprecator', () => {
);
}
});

it('registers a deprecation entry for installation.duplicateDeviceTokenActionEnforceAuth', () => {
const Deprecations = require('../lib/Deprecator/Deprecations');
const entry = Deprecations.find(
d => d.optionKey === 'installation.duplicateDeviceTokenActionEnforceAuth'
);
expect(entry).toBeDefined();
expect(entry.changeNewDefault).toBe('true');
expect(entry.solution).toContain('duplicateDeviceTokenActionEnforceAuth');
});

it('logs deprecation for installation.duplicateDeviceTokenActionEnforceAuth when not set', async () => {
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});

await reconfigureServer();
expect(logSpy).toHaveBeenCalledWith(
jasmine.objectContaining({
optionKey: 'installation.duplicateDeviceTokenActionEnforceAuth',
changeNewDefault: 'true',
})
);
});

it('does not log deprecation for installation.duplicateDeviceTokenActionEnforceAuth when explicitly set', async () => {
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});

await reconfigureServer({
installation: { duplicateDeviceTokenActionEnforceAuth: false },
});
expect(logSpy).not.toHaveBeenCalledWith(
jasmine.objectContaining({
optionKey: 'installation.duplicateDeviceTokenActionEnforceAuth',
})
);
});
});
Loading
Loading