diff --git a/src/mol-model/structure/export/mmcif.ts b/src/mol-model/structure/export/mmcif.ts
index ea699f649fe5e900813d315af5cad2717b39bbb5..bf93893f0286359b919b70b8eb60620dc048768e 100644
--- a/src/mol-model/structure/export/mmcif.ts
+++ b/src/mol-model/structure/export/mmcif.ts
@@ -11,7 +11,7 @@ import { Structure, Element } from '../structure'
 import { Model } from '../model'
 import P from '../query/properties'
 
-interface Context {
+export interface CifExportContext {
     structure: Structure,
     model: Model
 }
@@ -53,7 +53,7 @@ const atom_site_fields: CifField<Element.Location>[] = [
 ];
 
 function copy_mmCif_cat(name: keyof mmCIF_Schema) {
-    return ({ model }: Context) => {
+    return ({ model }: CifExportContext) => {
         if (model.sourceData.kind !== 'mmCIF') return CifCategory.Empty;
         const table = model.sourceData.data[name];
         if (!table || !table._rowCount) return CifCategory.Empty;
@@ -61,12 +61,12 @@ function copy_mmCif_cat(name: keyof mmCIF_Schema) {
     };
 }
 
-function _entity({ model, structure }: Context): CifCategory {
+function _entity({ model, structure }: CifExportContext): CifCategory {
     const keys = Structure.getEntityKeys(structure);
     return CifCategory.ofTable('entity', model.entities.data, keys);
 }
 
-function _atom_site({ structure }: Context): CifCategory {
+function _atom_site({ structure }: CifExportContext): CifCategory {
     return {
         data: structure,
         name: 'atom_site',
@@ -104,7 +104,7 @@ export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structure: S
     if (models.length !== 1) throw 'Can\'t export stucture composed from multiple models.';
     const model = models[0];
 
-    const ctx: Context[] = [{ structure, model }];
+    const ctx: CifExportContext[] = [{ structure, model }];
 
     for (const cat of Categories) {
         encoder.writeCategory(cat, ctx);
diff --git a/src/mol-model/structure/model/formats/mmcif.ts b/src/mol-model/structure/model/formats/mmcif.ts
index abd3a7a5959269a8bd6d98fef96eb57f3efd7a4a..66c2bfc281ce22ddf1f1f31b560455a5bc0a2c26 100644
--- a/src/mol-model/structure/model/formats/mmcif.ts
+++ b/src/mol-model/structure/model/formats/mmcif.ts
@@ -24,6 +24,7 @@ import { getSequence } from './mmcif/sequence';
 import { sortAtomSite } from './mmcif/sort';
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { Element } from '../../../structure'
+import { CustomProperties } from '../properties/custom';
 
 import mmCIF_Format = Format.mmCIF
 type AtomSite = mmCIF_Database['atom_site']
@@ -184,6 +185,7 @@ function createModel(format: mmCIF_Format, atom_site: AtomSite, previous?: Model
         sourceData: format,
         modelNum: format.data.atom_site.pdbx_PDB_model_num.value(0),
         entities,
+        symmetry: getSymmetry(format),
         atomicHierarchy,
         sequence: getSequence(format.data, entities, atomicHierarchy, modifiedResidueNameMap),
         atomicConformation: getConformation(atom_site),
@@ -193,7 +195,9 @@ function createModel(format: mmCIF_Format, atom_site: AtomSite, previous?: Model
             secondaryStructure: getSecondaryStructureMmCif(format.data, atomicHierarchy),
             modifiedResidueNameMap
         },
-        symmetry: getSymmetry(format)
+        customProperties: new CustomProperties(),
+        _staticPropertyData: Object.create(null),
+        _dynamicPropertyData: Object.create(null)
     };
 }
 
diff --git a/src/mol-model/structure/model/formats/mmcif/bonds.ts b/src/mol-model/structure/model/formats/mmcif/bonds.ts
index 4eb62528687553ac5ba1ff9afb74d2ea0d31afa2..5dd2180cec663da83adfeaca2b3b791d385de5de 100644
--- a/src/mol-model/structure/model/formats/mmcif/bonds.ts
+++ b/src/mol-model/structure/model/formats/mmcif/bonds.ts
@@ -10,6 +10,8 @@ import { LinkType } from '../../types'
 import { findEntityIdByAsymId, findAtomIndexByLabelName } from './util'
 import { Column } from 'mol-data/db'
 
+// TODO: add dynamic property descriptor for this?
+
 export interface StructConn {
     getResidueEntries(residueAIndex: number, residueBIndex: number): ReadonlyArray<StructConn.Entry>
     getAtomEntries(atomIndex: number): ReadonlyArray<StructConn.Entry>
@@ -100,7 +102,7 @@ export namespace StructConn {
 
     export const PropName = '__StructConn__';
     export function fromModel(model: Model): StructConn | undefined {
-        if (model.properties[PropName]) return model.properties[PropName];
+        if (model._staticPropertyData[PropName]) return model._staticPropertyData[PropName];
 
         if (model.sourceData.kind !== 'mmCIF') return;
         const { struct_conn } = model.sourceData.data;
@@ -189,7 +191,7 @@ export namespace StructConn {
         }
 
         const ret = new StructConnImpl(entries);
-        model.properties[PropName] = ret;
+        model._staticPropertyData[PropName] = ret;
         return ret;
     }
 }
@@ -230,7 +232,7 @@ export namespace ComponentBond {
 
     export const PropName = '__ComponentBond__';
     export function fromModel(model: Model): ComponentBond | undefined {
-        if (model.properties[PropName]) return model.properties[PropName];
+        if (model._staticPropertyData[PropName]) return model._staticPropertyData[PropName];
 
         if (model.sourceData.kind !== 'mmCIF') return
         const { chem_comp_bond } = model.sourceData.data;
@@ -269,7 +271,7 @@ export namespace ComponentBond {
             entry.add(nameA, nameB, ord, flags);
         }
 
-        model.properties[PropName] = compBond;
+        model._staticPropertyData[PropName] = compBond;
         return compBond;
     }
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts
index 6ade4fe45b03ca1b3642693475aadf7603e2b4e4..bce90da7e9885f370bcab28e772cfbc4d97bd9b0 100644
--- a/src/mol-model/structure/model/model.ts
+++ b/src/mol-model/structure/model/model.ts
@@ -11,6 +11,7 @@ import { AtomicHierarchy, AtomicConformation } from './properties/atomic'
 import { ModelSymmetry } from './properties/symmetry'
 import { CoarseHierarchy, CoarseConformation } from './properties/coarse'
 import { Entities } from './properties/common';
+import { CustomProperties } from './properties/custom';
 import { SecondaryStructure } from './properties/seconday-structure';
 
 //import from_gro from './formats/gro'
@@ -35,15 +36,21 @@ interface Model extends Readonly<{
     atomicHierarchy: AtomicHierarchy,
     atomicConformation: AtomicConformation,
 
-    /** Various parts of the code can "cache" custom properties here */
     properties: {
+        // secondary structure provided by the input file
         readonly secondaryStructure: SecondaryStructure,
         // maps modified residue name to its parent
-        readonly modifiedResidueNameMap: Map<string, string>,
-        [customName: string]: any
+        readonly modifiedResidueNameMap: Map<string, string>
     },
 
-    // TODO: separate properties to "static" (propagated with trajectory) and "dynamic" (computed for each frame separately)
+    customProperties: CustomProperties,
+
+    /**
+     * Not to be accessed directly, each custom property descriptor
+     * defines property accessors that use this field to store the data.
+     */
+    _staticPropertyData: { [name: string]: any },
+    _dynamicPropertyData: { [name: string]: any },
 
     coarseHierarchy: CoarseHierarchy,
     coarseConformation: CoarseConformation
diff --git a/src/mol-model/structure/model/properties/custom.ts b/src/mol-model/structure/model/properties/custom.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d9a06eee1c7a5f4e19ae8cc462117427a020ca4a
--- /dev/null
+++ b/src/mol-model/structure/model/properties/custom.ts
@@ -0,0 +1,8 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export * from './custom/descriptor'
+export * from './custom/collection'
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/custom/collection.ts b/src/mol-model/structure/model/properties/custom/collection.ts
new file mode 100644
index 0000000000000000000000000000000000000000..75dda9b2da0f293d69f2f538a25e531c7ebb4780
--- /dev/null
+++ b/src/mol-model/structure/model/properties/custom/collection.ts
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PropertyDescriptor } from './descriptor'
+
+export class CustomProperties {
+    private _list: PropertyDescriptor[] = [];
+    private _set = new Set<PropertyDescriptor>();
+
+    get all(): ReadonlyArray<PropertyDescriptor> {
+        return this._list;
+    }
+
+    add(desc: PropertyDescriptor) {
+        this._list.push(desc);
+        this._set.add(desc);
+    }
+
+    has(desc: PropertyDescriptor): boolean {
+        return this._set.has(desc);
+    }
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/custom/descriptor.ts b/src/mol-model/structure/model/properties/custom/descriptor.ts
new file mode 100644
index 0000000000000000000000000000000000000000..37af3562beddfa684797fb84c8459a75b1b0a332
--- /dev/null
+++ b/src/mol-model/structure/model/properties/custom/descriptor.ts
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CifWriter } from 'mol-io/writer/cif'
+import { CifExportContext } from '../../../export/mmcif';
+
+interface PropertyDescriptor {
+    readonly isStatic: boolean,
+    readonly name: string,
+
+    /** Given a structure, returns a list of category providers used for export. */
+    getCifCategories: (ctx: CifExportContext) => CifWriter.Category.Provider[]
+}
+
+export { PropertyDescriptor }
\ No newline at end of file